From afe49de9c2af046b35e49f70ee59e3e4ba2cdb2f Mon Sep 17 00:00:00 2001 From: Rishabh Garg Date: Wed, 23 Mar 2022 16:31:59 +0530 Subject: [PATCH] Refined UI/UX for Portal flows (#234) On the heels of multiple tiers going GA, this change brings a massive visual overhaul to Portal for almost all pages and flows, along with adding consistency between different multiple tier flows. It also overhauls the tests to match our new UI/UX for Portal. Co-authored-by: Peter Zimon --- ghost/portal/src/App.js | 2 +- ghost/portal/src/components/Frame.styles.js | 1386 +++++++++-------- ghost/portal/src/components/Global.styles.js | 56 +- .../src/components/Notification.styles.js | 2 +- ghost/portal/src/components/PopupModal.js | 50 +- .../src/components/common/ActionButton.js | 1 - .../src/components/common/BackButton.js | 7 +- .../src/components/common/InputField.js | 11 +- .../src/components/common/PlansSection.js | 430 +---- .../components/common/PlansSection.test.js | 39 - .../components/common/PopupNotification.js | 54 +- .../portal/src/components/common/PoweredBy.js | 15 + .../src/components/common/ProductsSection.js | 904 +++++------ .../components/common/SiteTitleBackButton.js | 15 + ghost/portal/src/components/common/Switch.js | 22 +- .../src/components/pages/AccountHomePage.js | 11 +- .../src/components/pages/AccountPlanPage.js | 72 +- .../components/pages/AccountPlanPage.test.js | 22 +- .../portal/src/components/pages/OfferPage.js | 388 ++--- .../portal/src/components/pages/SigninPage.js | 26 +- .../portal/src/components/pages/SignupPage.js | 557 +++---- .../src/components/pages/SignupPage.test.js | 10 +- ghost/portal/src/images/icons/checkmark.svg | 4 +- ghost/portal/src/images/icons/close.svg | 2 +- ghost/portal/src/tests/SignupFlow.test.js | 85 +- ghost/portal/src/tests/UpgradeFlow.test.js | 23 +- .../portal/src/tests/data-attributes.test.js | 2 +- ghost/portal/src/tests/portal-links.test.js | 2 +- ghost/portal/src/utils/fixtures-generator.js | 4 +- ghost/portal/src/utils/fixtures.js | 30 +- ghost/portal/src/utils/helpers.js | 13 +- 31 files changed, 1841 insertions(+), 2404 deletions(-) delete mode 100644 ghost/portal/src/components/common/PlansSection.test.js create mode 100644 ghost/portal/src/components/common/PoweredBy.js create mode 100644 ghost/portal/src/components/common/SiteTitleBackButton.js diff --git a/ghost/portal/src/App.js b/ghost/portal/src/App.js index f4638b402d..f07d315166 100644 --- a/ghost/portal/src/App.js +++ b/ghost/portal/src/App.js @@ -18,7 +18,7 @@ const React = require('react'); const DEV_MODE_DATA = { showPopup: true, site: Fixtures.site, - member: Fixtures.member.paid, + member: Fixtures.member.free, page: 'accountHome', ...Fixtures.paidMemberOnTier(), pageData: Fixtures.offer diff --git a/ghost/portal/src/components/Frame.styles.js b/ghost/portal/src/components/Frame.styles.js index c11bf5024a..068436a6cc 100644 --- a/ghost/portal/src/components/Frame.styles.js +++ b/ghost/portal/src/components/Frame.styles.js @@ -20,333 +20,680 @@ import {OfferPageStyles} from './pages/OfferPage'; // Global styles const FrameStyles = ` - .gh-portal-main-title { - text-align: center; - color: var(--grey0); - line-height: 1.35em; - } +.gh-portal-main-title { + text-align: center; + color: var(--grey0); + line-height: 1.1em; +} - .gh-portal-text-disabled { - color: var(--grey3); - font-weight: normal; - opacity: 0.35; - } +.gh-portal-text-disabled { + color: var(--grey3); + font-weight: normal; + opacity: 0.35; +} - .gh-portal-text-center { - text-align: center; - } +.gh-portal-text-center { + text-align: center; +} - .gh-portal-input-label { - color: var(--grey1); - font-size: 1.3rem; - font-weight: 500; - margin-bottom: 2px; - letter-spacing: 0.35px; - } +.gh-portal-input-label { + color: var(--grey1); + font-size: 1.3rem; + font-weight: 600; + margin-bottom: 2px; + letter-spacing: 0px; +} - .gh-portal-setting-data { - color: var(--grey6); - font-size: 1.3rem; - line-height: 1.15em; - } +.gh-portal-setting-data { + color: var(--grey6); + font-size: 1.3rem; + line-height: 1.15em; +} - .gh-portal-error { - color: var(--red); - font-size: 1.4rem; - line-height: 1.6em; - margin: 12px 0; - } +.gh-portal-error { + color: var(--red); + font-size: 1.4rem; + line-height: 1.6em; + margin: 12px 0; +} - /* Buttons - /* ----------------------------------------------------- */ - .gh-portal-btn { - position: relative; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.4rem; - font-weight: 500; - line-height: 1em; - letter-spacing: 0.2px; - text-align: center; - white-space: nowrap; - text-decoration: none; - color: var(--grey0); - background: var(--white); - border: 1px solid var(--grey12); - min-width: 80px; - height: 42px; - padding: 0 1.8rem; - border-radius: 4px; - cursor: pointer; - transition: all .25s ease; - box-shadow: none; - box-shadow: 0 2px 6px -3px rgba(0,0,0,0.19); - user-select: none; - outline: none; - } +/* Buttons +/* ----------------------------------------------------- */ +.gh-portal-btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: 500; + line-height: 1em; + letter-spacing: 0.2px; + text-align: center; + white-space: nowrap; + text-decoration: none; + color: var(--grey0); + background: var(--white); + border: 1px solid var(--grey12); + min-width: 80px; + height: 44px; + padding: 0 1.8rem; + border-radius: 6px; + cursor: pointer; + transition: all .25s ease; + box-shadow: none; + user-select: none; + outline: none; +} - .gh-portal-btn:hover { - border-color: var(--grey10); - } +.gh-portal-btn:hover { + border-color: var(--grey10); +} - .gh-portal-btn:disabled { - opacity: 0.3 !important; - cursor: auto; - } +.gh-portal-btn:disabled { + opacity: 0.5 !important; + cursor: auto; +} - .gh-portal-btn-icon svg { - width: 16px; - height: 16px; - margin-right: 4px; - stroke: currentColor; - } +.gh-portal-btn-container.sticky { + transition: none; + position: sticky; + bottom: 0; + margin: 0 0 -32px; + padding: 32px 0 32px; + background: linear-gradient(0deg, rgba(255,255,255,1) 75%, rgba(255,255,255,0) 100%); +} - .gh-portal-btn-icon svg path { - stroke: currentColor; - } +.gh-portal-btn-container.sticky.m28 { + margin: 0 0 -28px; + padding: 28px 0 28px; +} - .gh-portal-btn-link { - line-height: 1; - background: none; - padding: 0; - height: unset; - min-width: unset; - box-shadow: none; - border: none; - } +.gh-portal-btn-container.sticky.m24 { + margin: 0 0 -24px; + padding: 24px 0 24px; +} - .gh-portal-btn-link:hover { - box-shadow: none; - opacity: 0.85; - } +.gh-portal-btn-container .gh-portal-btn { + margin: 0; +} - .gh-portal-btn-branded { - color: var(--brandcolor); - } +.gh-portal-btn-icon svg { + width: 16px; + height: 16px; + margin-right: 4px; + stroke: currentColor; +} - .gh-portal-btn-list { - font-size: 1.4rem; - color: var(--brandcolor); - height: 38px; - width: unset; - min-width: unset; - padding: 0 4px; - margin: 0 -4px; - box-shadow: none; - border: none; - } +.gh-portal-btn-icon svg path { + stroke: currentColor; +} - .gh-portal-btn-list:hover { - box-shadow: none; - opacity: 0.75; - } +.gh-portal-btn-link { + line-height: 1; + background: none; + padding: 0; + height: unset; + min-width: unset; + box-shadow: none; + border: none; +} - .gh-portal-btn-logout { - position: absolute; - top: 22px; - left: 24px; - background: none; - border: none; - height: unset; - color: var(--grey3); - padding: 0; - margin: 0; - z-index: 999; - box-shadow: none; - } +.gh-portal-btn-link:hover { + box-shadow: none; + opacity: 0.85; +} - .gh-portal-btn-logout .label { - opacity: 0; - transform: translateX(-6px); - transition: all 0.2s ease-in-out; - } +.gh-portal-btn-branded { + color: var(--brandcolor); +} - .gh-portal-btn-logout:hover { - padding: 0; - margin: 0; - background: none; - border: none; - height: unset; - box-shadow: none; - } +.gh-portal-btn-list { + font-size: 1.5rem; + color: var(--brandcolor); + height: 38px; + width: unset; + min-width: unset; + padding: 0 4px; + margin: 0 -4px; + box-shadow: none; + border: none; +} - .gh-portal-btn-logout:hover .label { - opacity: 1.0; - transform: translateX(-4px); - } +.gh-portal-btn-list:hover { + box-shadow: none; + opacity: 0.75; +} - .gh-portal-logouticon { - color: var(--grey9); - cursor: pointer; - width: 23px; - height: 23px; - padding: 6px; - transform: translateX(0); - transition: all 0.2s ease-in-out; - } +.gh-portal-btn-logout { + position: absolute; + top: 22px; + left: 24px; + background: none; + border: none; + height: unset; + color: var(--grey3); + padding: 0; + margin: 0; + z-index: 999; + box-shadow: none; +} - .gh-portal-logouticon path { - stroke: var(--grey9); - transition: all 0.2s ease-in-out; - } +.gh-portal-btn-logout .label { + opacity: 0; + transform: translateX(-6px); + transition: all 0.2s ease-in-out; +} - .gh-portal-btn-logout:hover .gh-portal-logouticon { - transform: translateX(-2px); - } +.gh-portal-btn-logout:hover { + padding: 0; + margin: 0; + background: none; + border: none; + height: unset; + box-shadow: none; +} - .gh-portal-btn-logout:hover .gh-portal-logouticon path { - stroke: var(--grey3); - } +.gh-portal-btn-logout:hover .label { + opacity: 1.0; + transform: translateX(-4px); +} - /* Global layout styles - /* ----------------------------------------------------- */ - .gh-portal-popup-background { - position: absolute; - display: block; - top: 0; - right: 0; - bottom: 0; - left: 0; - animation: fadein 0.2s; - background: linear-gradient(315deg , rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.3) 100%); - backdrop-filter: blur(2px); - -webkit-backdrop-filter: blur(2px); - -webkit-transform: translate3d(0, 0, 0); - -moz-transform: translate3d(0, 0, 0); - -ms-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } +.gh-portal-btn-site-title-back { + transition: transform 0.25s ease-in-out; + z-index: 10000; +} - .gh-portal-popup-background.preview { - background: #EDF0F2; - animation: none; - pointer-events: none; - } +.gh-portal-btn-site-title-back:hover { + transform: translateX(-6px); +} - @keyframes fadein { - 0% { opacity: 0; } - 100%{ opacity: 1.0; } - } - - .gh-portal-popup-wrapper { - position: relative; - padding: 5vmin 0 0; - height: 100%; - max-height: 100vh; - overflow: auto; - } - - /* Hiding scrollbars */ - .gh-portal-popup-wrapper { - padding-right: 30px !important; - margin-right: -30px !important; - -ms-overflow-style: none; - scrollbar-width: none; - } - - .gh-portal-popup-wrapper::-webkit-scrollbar { +@media (max-width: 960px) { + .gh-portal-btn-site-title-back { display: none; } +} - .gh-portal-popup-container { - outline: none; - position: relative; - display: flex; - box-sizing: border-box; - flex-direction: column; - justify-content: flex-start; - overflow: hidden; - font-size: 1.5rem; - text-align: left; - letter-spacing: 0; - text-rendering: optimizeLegibility; - background: var(--white); - width: 420px; - margin: 0 auto 40px; - transform: translateY(20px); - border-radius: 5px; - box-shadow: 0 3.8px 2.2px rgba(0, 0, 0, 0.028), 0 9.2px 5.3px rgba(0, 0, 0, 0.04), 0 17.3px 10px rgba(0, 0, 0, 0.05), 0 30.8px 17.9px rgba(0, 0, 0, 0.06), 0 57.7px 33.4px rgba(0, 0, 0, 0.072), 0 138px 80px rgba(0, 0, 0, 0.1); - animation: popup 0.25s ease-in-out; - z-index: 9999; +.gh-portal-logouticon { + color: var(--grey9); + cursor: pointer; + width: 23px; + height: 23px; + padding: 6px; + transform: translateX(0); + transition: all 0.2s ease-in-out; +} + +.gh-portal-logouticon path { + stroke: var(--grey9); + transition: all 0.2s ease-in-out; +} + +.gh-portal-btn-logout:hover .gh-portal-logouticon { + transform: translateX(-2px); +} + +.gh-portal-btn-logout:hover .gh-portal-logouticon path { + stroke: var(--grey3); +} + +/* Global layout styles +/* ----------------------------------------------------- */ +.gh-portal-popup-background { + position: absolute; + display: block; + top: 0; + right: 0; + bottom: 0; + left: 0; + animation: fadein 0.2s; + background: linear-gradient(315deg , rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.1) 100%); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} + +.gh-portal-popup-background.preview { + background: #EDF0F2; + animation: none; + pointer-events: none; +} + +@keyframes fadein { + 0% { opacity: 0; } + 100%{ opacity: 1.0; } +} + +.gh-portal-popup-wrapper { + position: relative; + padding: 5vmin 0 0; + height: 100%; + max-height: 100vh; + overflow: scroll; +} + +/* Hiding scrollbars */ +.gh-portal-popup-wrapper { + padding-right: 30px !important; + margin-right: -30px !important; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.gh-portal-popup-wrapper::-webkit-scrollbar { + display: none; +} + +.gh-portal-popup-wrapper.full-size { + height: 100vh; + padding: 0; +} + +.gh-portal-popup-container { + outline: none; + position: relative; + display: flex; + box-sizing: border-box; + flex-direction: column; + justify-content: flex-start; + font-size: 1.5rem; + text-align: left; + letter-spacing: 0; + text-rendering: optimizeLegibility; + background: var(--white); + width: 500px; + margin: 0 auto 40px; + padding: 32px; + transform: translateY(0px); + border-radius: 10px; + box-shadow: 0 3.8px 2.2px rgba(0, 0, 0, 0.028), 0 9.2px 5.3px rgba(0, 0, 0, 0.04), 0 17.3px 10px rgba(0, 0, 0, 0.05), 0 30.8px 17.9px rgba(0, 0, 0, 0.06), 0 57.7px 33.4px rgba(0, 0, 0, 0.072), 0 138px 80px rgba(0, 0, 0, 0.1); + animation: popup 0.25s ease-in-out; + z-index: 9999; +} + +.gh-portal-popup-container.full-size { + width: 100vw; + min-height: 100vh; + justify-content: center; + animation: popup-full-size 0.25s ease-in-out; + margin: 0; + border-radius: 0; + transform: translateY(0px); + transform-origin: top; + padding: 2vmin 6vmin; + padding-bottom: 4vw; +} + +.gh-portal-popup-container.preview { + animation: none !important; +} + +@keyframes popup { + 0% { + transform: translateY(-30px); + opacity: 0; } - - @keyframes popup { - 0% { - transform: scale(0.9) translateY(0px); - opacity: 0; - } - 75% { - opacity: 1.0; - } - 100%{ - transform: scale(1) translateY(20px); - } + 1% { + transform: translateY(30px); + opacity: 0; } - - @media (max-height: 670px) { - .gh-portal-popup-container { - transform-origin: top; - } + 100%{ + transform: translateY(0); + opacity: 1.0; } +} - .gh-portal-powered { - position: absolute; - bottom: 24px; - left: 24px; +@keyframes popup-full-size { + 0% { + transform: translateY(0px); + opacity: 0; } - - .gh-portal-powered a { - border: none; - display: flex; - align-items: center; - line-height: 0; - border-radius: 4px; - background: #ffffff; - padding: 6px 8px 6px 7px; - color: #303336; - font-size: 1.25rem; - letter-spacing: -0.2px; - font-weight: 500; - text-decoration: none; - transition: color 0.5s ease-in-out; - width: 146px; - height: 28px; - line-height: 28px; + 1% { + transform: translateY(30px); + opacity: 0; } - - .gh-portal-powered a:hover { - color: #15171A; + 100%{ + transform: translateY(0); + opacity: 1.0; } +} - @keyframes powered-fade-in { - 0% { - transform: scale(0.98); - opacity: 0; - } - 75% { - opacity: 1.0; - } - 100%{ - transform: scale(1); - } +.gh-portal-powered { + position: absolute; + bottom: 24px; + left: 24px; + z-index: 9999; +} + +.gh-portal-powered a { + border: none; + display: flex; + align-items: center; + line-height: 0; + border-radius: 4px; + background: #ffffff; + padding: 6px 8px 6px 7px; + color: #303336; + font-size: 1.25rem; + letter-spacing: -0.2px; + font-weight: 500; + text-decoration: none; + transition: color 0.5s ease-in-out; + width: 146px; + height: 28px; + line-height: 28px; +} + +.gh-portal-powered a:hover { + color: #15171A; +} + +@keyframes powered-fade-in { + 0% { + transform: scale(0.98); + opacity: 0; } - - .gh-portal-powered a svg { - height: 16px; - width: 16px; - margin: 0 6px 0 0; + 75% { + opacity: 1.0; } - - .gh-portal-container-wide { - width: 440px; + 100%{ + transform: scale(1); } +} - .gh-portal-container-narrow { - width: 380px; - } +.gh-portal-powered a svg { + height: 16px; + width: 16px; + margin: 0 6px 0 0; +} - .gh-portal-popup-container.preview { +.gh-portal-powered.outside.full-size { + display: none; +} + +/* Sets the main content area of the popup scrollable. +/* 12vw is the sum horizontal padding of the popup container +*/ +.gh-portal-content { + position: relative; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.gh-portal-content::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.gh-portal-content { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.gh-portal-closeicon-container { + position: fixed; + top: 24px; + right: 24px; + z-index: 10000; +} + +.gh-portal-closeicon { + color: var(--grey10); + cursor: pointer; + width: 20px; + height: 20px; + padding: 12px; + transition: all 0.2s ease-in-out; +} + +.gh-portal-closeicon:hover { + color: var(--grey5); +} + +.gh-portal-popup-wrapper.full-size .gh-portal-closeicon-container, +.gh-portal-popup-container.full-size .gh-portal-closeicon-container { + top: 20px; + right: 20px; +} + +.gh-portal-popup-wrapper.full-size .gh-portal-closeicon, +.gh-portal-popup-container.full-size .gh-portal-closeicon { + color: var(--grey6); + width: 24px; + height: 24px; +} + +.gh-portal-logout-container { + position: absolute; + top: 8px; + left: 8px; +} + +.gh-portal-header { + display: flex; + flex-direction: column; + align-items: center; + padding-bottom: 24px; +} + +.gh-portal-section { + margin-bottom: 40px; +} + +.gh-portal-section.form { + margin-bottom: 20px; +} + +.gh-portal-detail-header { + position: relative; + display: flex; + align-items: center; + justify-content: center; + margin: -2px 0 40px; +} + +.gh-portal-detail-footer .gh-portal-btn { + min-width: 90px; +} + +.gh-portal-action-footer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.gh-portal-list-header { + font-size: 1.25rem; + font-weight: 500; + color: var(--grey3); + text-transform: uppercase; + letter-spacing: 0.2px; + line-height: 1.7em; + margin-bottom: 4px; +} + +.gh-portal-list + .gh-portal-list-header { + margin-top: 28px; +} + +.gh-portal-list { + background: var(--white); + padding: 20px; + border-radius: 8px; + border: 1px solid var(--grey12); +} + +.gh-portal-list section { + display: flex; + align-items: center; + margin: 0 -20px 20px; + padding: 0 20px 20px; + border-bottom: 1px solid var(--grey12); +} + +.gh-portal-list section:last-of-type { + margin-bottom: 0; + padding-bottom: 0; + border: none; +} + +.gh-portal-list-detail { + flex-grow: 1; +} + +.gh-portal-list-detail h3 { + font-size: 1.5rem; + font-weight: 600; +} + +.gh-portal-list-detail p { + font-size: 1.45rem; + letter-spacing: 0.3px; + line-height: 1.3em; + padding: 0; + margin: 5px 0 0; + color: var(--grey6); +} + +.gh-portal-list-detail .old-price { + text-decoration: line-through; +} + +.gh-portal-right-arrow { + line-height: 1; + color: var(--grey8); +} + +.gh-portal-right-arrow svg { + width: 17px; + height: 17px; + margin-top: 1px; + margin-right: -6px; +} + +.gh-portal-expire-warning { + text-align: center; + color: var(--red); + font-weight: 500; + font-size: 1.4rem; + margin: 12px 0; +} + +.gh-portal-cookiebanner { + background: var(--red); + color: var(--white); + text-align: center; + font-size: 1.4rem; + letter-spacing: 0.2px; + line-height: 1.4em; + padding: 8px; +} + +/* Icons +/* ----------------------------------------------------- */ +.gh-portal-icon { + color: var(--brandcolor); +} + +/* Spacing modifiers +/* ----------------------------------------------------- */ +.gh-portal-strong { font-weight: 600; } + +.mt1 { margin-top: 4px; } +.mt2 { margin-top: 8px; } +.mt3 { margin-top: 12px; } +.mt4 { margin-top: 16px; } +.mt5 { margin-top: 20px; } +.mt6 { margin-top: 24px; } +.mt7 { margin-top: 28px; } +.mt8 { margin-top: 32px; } +.mt9 { margin-top: 36px; } +.mt10 { margin-top: 40px; } + +.mr1 { margin-right: 4px; } +.mr2 { margin-right: 8px; } +.mr3 { margin-right: 12px; } +.mr4 { margin-right: 16px; } +.mr5 { margin-right: 20px; } +.mr6 { margin-right: 24px; } +.mr7 { margin-right: 28px; } +.mr8 { margin-right: 32px; } +.mr9 { margin-right: 36px; } +.mr10 { margin-right: 40px; } + +.mb1 { margin-bottom: 4px; } +.mb2 { margin-bottom: 8px; } +.mb3 { margin-bottom: 12px; } +.mb4 { margin-bottom: 16px; } +.mb5 { margin-bottom: 20px; } +.mb6 { margin-bottom: 24px; } +.mb7 { margin-bottom: 28px; } +.mb8 { margin-bottom: 32px; } +.mb9 { margin-bottom: 36px; } +.mb10 { margin-bottom: 40px; } + +.ml1 { margin-left: 4px; } +.ml2 { margin-left: 8px; } +.ml3 { margin-left: 12px; } +.ml4 { margin-left: 16px; } +.ml5 { margin-left: 20px; } +.ml6 { margin-left: 24px; } +.ml7 { margin-left: 28px; } +.ml8 { margin-left: 32px; } +.ml9 { margin-left: 36px; } +.ml10 { margin-left: 40px; } + +.pt1 { padding-top: 4px; } +.pt2 { padding-top: 8px; } +.pt3 { padding-top: 12px; } +.pt4 { padding-top: 16px; } +.pt5 { padding-top: 20px; } +.pt6 { padding-top: 24px; } +.pt7 { padding-top: 28px; } +.pt8 { padding-top: 32px; } +.pt9 { padding-top: 36px; } +.pt10 { padding-top: 40px; } + +.pr1 { padding-right: 4px; } +.pr2 { padding-right: 8px; } +.pr3 { padding-right: 12px; } +.pr4 { padding-right: 16px; } +.pr5 { padding-right: 20px; } +.pr6 { padding-right: 24px; } +.pr7 { padding-right: 28px; } +.pr8 { padding-right: 32px; } +.pr9 { padding-right: 36px; } +.pr10 { padding-right: 40px; } + +.pb1 { padding-bottom: 4px; } +.pb2 { padding-bottom: 8px; } +.pb3 { padding-bottom: 12px; } +.pb4 { padding-bottom: 16px; } +.pb5 { padding-bottom: 20px; } +.pb6 { padding-bottom: 24px; } +.pb7 { padding-bottom: 28px; } +.pb8 { padding-bottom: 32px; } +.pb9 { padding-bottom: 36px; } +.pb10 { padding-bottom: 40px; } + +.pl1 { padding-left: 4px; } +.pl2 { padding-left: 8px; } +.pl3 { padding-left: 12px; } +.pl4 { padding-left: 16px; } +.pl5 { padding-left: 20px; } +.pl6 { padding-left: 24px; } +.pl7 { padding-left: 28px; } +.pl8 { padding-left: 32px; } +.pl9 { padding-left: 36px; } +.pl10 { padding-left: 40px; } + +.hidden { display: none !important; } +`; + +const MobileStyles = ` +@media (min-width: 520px) { + .gh-portal-popup-wrapper.full-size .gh-portal-popup-container.preview { box-shadow: 0 0 0 1px rgba(0,0,0,0.02), 0 2.8px 2.2px rgba(0, 0, 0, 0.02), @@ -356,297 +703,48 @@ const FrameStyles = ` 0 41.8px 33.4px rgba(0, 0, 0, 0.05), 0 100px 80px rgba(0, 0, 0, 0.07); animation: none; - margin-bottom: 0; + margin: 32px; + padding: 32px 32px 0; + width: calc(100vw - 64px); + height: calc(100vh - 160px); + min-height: unset; + border-radius: 12px; + overflow: auto; + justify-content: flex-start; + } +} + +@media (max-width: 1440px) { + .gh-portal-popup-container:not(.full-size):not(.preview) { + width: 460px; } - /* Sets the main content area of the popup scrollable. - /* 12vw is the sum horizontal padding of the popup container - */ - .gh-portal-content { + .gh-portal-input { + height: 42px; + margin-bottom: 16px; + } + + button[class="gh-portal-btn"], + .gh-portal-btn-main, + .gh-portal-btn-primary { + height: 42px; + } +} + + +@media (max-width: 960px) { + .gh-portal-powered { + display: flex; position: relative; - overflow-y: scroll; - padding: 24px 32px 32px; - } - - .gh-portal-content.with-footer { - overflow-y: scroll; - padding-bottom: 0; - } - - /* Hide scrollbar for Chrome, Safari and Opera */ - .gh-portal-content::-webkit-scrollbar { - display: none; - } - - /* Hide scrollbar for IE, Edge and Firefox */ - .gh-portal-content { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ - } - - .gh-portal-popup-container footer { - padding: 0 32px 32px; - height: 72px; - } - - .gh-portal-closeicon-container { - position: absolute; - top: 14px; - right: 14px; - z-index: 999; - } - - .gh-portal-closeicon { - color: var(--grey9); - cursor: pointer; - width: 16px; - height: 16px; - padding: 12px; - transition: all 0.2s ease-in-out; - } - - .gh-portal-closeicon:hover { - color: var(--grey5); - } - - .gh-portal-logout-container { - position: absolute; - top: 8px; - left: 8px; - } - - .gh-portal-header { - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 24px; - } - - .gh-portal-section { - margin-bottom: 32px; - } - - .gh-portal-section.form { - margin-bottom: 20px; - } - - .gh-portal-detail-header { - position: relative; - display: flex; - align-items: center; - justify-content: center; - margin: -4px 0 24px; - } - - .gh-portal-detail-footer .gh-portal-btn { - min-width: 90px; - } - - .gh-portal-action-footer { - display: flex; - align-items: center; - justify-content: space-between; - } - - .gh-portal-list-header { - font-size: 1.25rem; - font-weight: 500; - color: var(--grey3); - text-transform: uppercase; - letter-spacing: 0.2px; - line-height: 1.7em; - margin-bottom: 4px; - } - - .gh-portal-list + .gh-portal-list-header { - margin-top: 28px; - } - - .gh-portal-list { + bottom: unset; + left: unset; background: var(--white); - padding: 20px; - border-radius: 3px; - box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.07), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.05); + justify-content: center; + width: 100%; + padding-top: 32px; } +} - .gh-portal-list.outline { - box-shadow: none; - border: 1px solid var(--grey12); - } - - .gh-portal-list section { - display: flex; - align-items: center; - margin: 0 -20px 20px; - padding: 0 20px 20px; - border-bottom: 1px solid var(--grey12); - } - - .gh-portal-list section:last-of-type { - margin-bottom: 0; - padding-bottom: 0; - border: none; - } - - .gh-portal-list-detail { - flex-grow: 1; - } - - .gh-portal-list-detail h3 { - font-size: 1.5rem; - font-weight: 500; - } - - .gh-portal-list-detail p { - font-size: 1.3rem; - letter-spacing: 0.3px; - line-height: 1.25em; - padding: 0; - margin: 4px 0 0; - color: var(--grey6); - } - - .gh-portal-list-detail .old-price { - text-decoration: line-through; - } - - .gh-portal-right-arrow { - line-height: 1; - color: var(--grey8); - } - - .gh-portal-right-arrow svg { - width: 17px; - height: 17px; - margin-top: 1px; - margin-right: -6px; - } - - .gh-portal-expire-warning { - text-align: center; - color: var(--red); - font-weight: 500; - font-size: 1.4rem; - margin: 12px 0; - } - - .gh-portal-cookiebanner { - background: var(--red); - color: var(--white); - text-align: center; - font-size: 1.4rem; - letter-spacing: 0.2px; - line-height: 1.4em; - padding: 8px; - } - - .gh-portal-popup-container.account-profile .gh-portal-action-footer { - height: unset; - padding-bottom: 36px; - } - - /* Icons - /* ----------------------------------------------------- */ - .gh-portal-icon { - color: var(--brandcolor); - } - - /* Spacing modifiers - /* ----------------------------------------------------- */ - .gh-portal-strong { font-weight: 600; } - - .mt1 { margin-top: 4px; } - .mt2 { margin-top: 8px; } - .mt3 { margin-top: 12px; } - .mt4 { margin-top: 16px; } - .mt5 { margin-top: 20px; } - .mt6 { margin-top: 24px; } - .mt7 { margin-top: 28px; } - .mt8 { margin-top: 32px; } - .mt9 { margin-top: 36px; } - .mt10 { margin-top: 40px; } - - .mr1 { margin-right: 4px; } - .mr2 { margin-right: 8px; } - .mr3 { margin-right: 12px; } - .mr4 { margin-right: 16px; } - .mr5 { margin-right: 20px; } - .mr6 { margin-right: 24px; } - .mr7 { margin-right: 28px; } - .mr8 { margin-right: 32px; } - .mr9 { margin-right: 36px; } - .mr10 { margin-right: 40px; } - - .mb1 { margin-bottom: 4px; } - .mb2 { margin-bottom: 8px; } - .mb3 { margin-bottom: 12px; } - .mb4 { margin-bottom: 16px; } - .mb5 { margin-bottom: 20px; } - .mb6 { margin-bottom: 24px; } - .mb7 { margin-bottom: 28px; } - .mb8 { margin-bottom: 32px; } - .mb9 { margin-bottom: 36px; } - .mb10 { margin-bottom: 40px; } - - .ml1 { margin-left: 4px; } - .ml2 { margin-left: 8px; } - .ml3 { margin-left: 12px; } - .ml4 { margin-left: 16px; } - .ml5 { margin-left: 20px; } - .ml6 { margin-left: 24px; } - .ml7 { margin-left: 28px; } - .ml8 { margin-left: 32px; } - .ml9 { margin-left: 36px; } - .ml10 { margin-left: 40px; } - - .pt1 { padding-top: 4px; } - .pt2 { padding-top: 8px; } - .pt3 { padding-top: 12px; } - .pt4 { padding-top: 16px; } - .pt5 { padding-top: 20px; } - .pt6 { padding-top: 24px; } - .pt7 { padding-top: 28px; } - .pt8 { padding-top: 32px; } - .pt9 { padding-top: 36px; } - .pt10 { padding-top: 40px; } - - .pr1 { padding-right: 4px; } - .pr2 { padding-right: 8px; } - .pr3 { padding-right: 12px; } - .pr4 { padding-right: 16px; } - .pr5 { padding-right: 20px; } - .pr6 { padding-right: 24px; } - .pr7 { padding-right: 28px; } - .pr8 { padding-right: 32px; } - .pr9 { padding-right: 36px; } - .pr10 { padding-right: 40px; } - - .pb1 { padding-bottom: 4px; } - .pb2 { padding-bottom: 8px; } - .pb3 { padding-bottom: 12px; } - .pb4 { padding-bottom: 16px; } - .pb5 { padding-bottom: 20px; } - .pb6 { padding-bottom: 24px; } - .pb7 { padding-bottom: 28px; } - .pb8 { padding-bottom: 32px; } - .pb9 { padding-bottom: 36px; } - .pb10 { padding-bottom: 40px; } - - .pl1 { padding-left: 4px; } - .pl2 { padding-left: 8px; } - .pl3 { padding-left: 12px; } - .pl4 { padding-left: 16px; } - .pl5 { padding-left: 20px; } - .pl6 { padding-left: 24px; } - .pl7 { padding-left: 28px; } - .pl8 { padding-left: 32px; } - .pl9 { padding-left: 36px; } - .pl10 { padding-left: 40px; } - - .hidden { display: none !important; } -`; - -const MobileStyles = ` @media (max-width: 480px) { .gh-portal-popup-wrapper { height: 100%; @@ -666,6 +764,11 @@ const MobileStyles = ` animation: popup-mobile 0.25s ease-in-out; box-shadow: none !important; transform: translateY(0); + padding: 28px !important; + } + + .gh-portal-popup-container.full-size { + justify-content: flex-start; } .gh-portal-popup-wrapper.account-home, @@ -673,124 +776,45 @@ const MobileStyles = ` background: var(--grey13); } - .gh-portal-popup-container.account-home .gh-portal-account-footer { - border-top: none; - padding-top: 0; + .gh-portal-popup-wrapper.full-size .gh-portal-closeicon, + .gh-portal-popup-container.full-size .gh-portal-closeicon { + width: 16px; + height: 16px; + } + + /* Small width preview in Admin */ + .gh-portal-popup-wrapper.preview:not(.full-size) footer.gh-portal-signup-footer, + .gh-portal-popup-wrapper.preview:not(.full-size) footer.gh-portal-signin-footer { + padding-bottom: 32px; + } + + .gh-portal-popup-container.preview:not(.full-size) { + max-height: 660px; + margin-bottom: 0; + } + + .gh-portal-popup-wrapper.preview.full-size { height: unset; + max-height: 660px; } - .gh-portal-popup-container:not(.multiple-products) .gh-portal-content { - position: relative !important; - overflow-y: auto !important; - max-height: unset !important; + .gh-portal-popup-container.preview.full-size { + max-height: 660px; + margin-bottom: 0; } - .gh-portal-powered { - display: flex; - position: relative; - bottom: unset; - left: unset; - background: var(--white); - justify-content: center; - width: 100%; - padding-top: 32px; + .preview .gh-portal-btn-container .gh-portal-signup-message, + .preview .gh-portal-invite-only-notification + .gh-portal-signup-message { + margin-bottom: 16px; } - .gh-portal-popup-wrapper.preview .gh-portal-powered, - .gh-portal-popup-wrapper.preview .gh-portal-powered a { - display: none !important; - } - - .gh-portal-popup-wrapper.account-home .gh-portal-powered { - background: var(--grey13); - } - - .gh-portal-powered a { - animation: none; - box-shadow: none; - background: none; - margin-bottom: 32px; - } - - .gh-portal-powered a svg { - opacity: 0.65; - } - - .gh-portal-popup-wrapper.account-home .gh-portal-powered a { - box-shadow: none; - } -} - -@media (max-width: 414px) { - .gh-portal-input { - height: 44px; - } - - .gh-portal-btn:not(.gh-portal-btn-link):not(.gh-portal-btn-back) { - min-height: 44px; + .preview .gh-portal-btn-container.sticky { + margin-bottom: 0; + padding-bottom: 0; } } @media (max-width: 390px) { - .gh-portal-plans-container:not(.has-multiple-products) { - flex-direction: column; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-section { - display: grid; - padding: 12px 10px; - grid-template-columns: 32px auto auto; - grid-template-rows: auto auto; - justify-items: start; - min-height: 60px; - border-right: none; - border-bottom: 1px solid var(--grey10); - } - - div:not(.gh-portal-product-card-header) > .gh-portal-plan-checkbox { - grid-column: 1 / 2; - grid-row: 1 / 3; - margin: 0 12px; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-pricelabel { - grid-column: 3 / 4; - grid-row: 1 / 3; - justify-self: end; - align-items: center; - margin: 0 4px 0 12px; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-name { - text-transform: none; - font-size: 1.4rem; - letter-spacing: 0.2px; - margin: 0; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-featurewrapper { - margin: 0; - padding: 0; - border: none; - align-items: flex-start; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-feature { - text-align: left; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-section:last-of-type { - border-bottom: none; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-section:first-of-type::before { - border-radius: 5px 5px 0 0; - } - - .gh-portal-plans-container:not(.has-multiple-products) .gh-portal-plan-section:last-of-type::before { - border-radius: 0 0 5px 5px; - } - .gh-portal-input { font-size: 1.4rem; margin-bottom: 16px; @@ -811,10 +835,6 @@ const MobileStyles = ` padding-bottom: 16px; } - .gh-portal-account-main { - padding: 24px 24px 0; - } - .gh-portal-powered { padding-top: 12px; } @@ -822,7 +842,7 @@ const MobileStyles = ` @media (min-width: 480px) and (max-height: 880px) { .gh-portal-popup-wrapper { - padding: 4vmin 0 72px; + padding: 4vmin 0 0; } } @@ -962,7 +982,7 @@ export function getFrameStyles({site}) { AvatarStyles + MagicLinkStyles + SignupPageStyles + - OfferPageStyles + + OfferPageStyles({site}) + PopupNotificationStyles + MobileStyles + MultipleProductsGlobalStyles; diff --git a/ghost/portal/src/components/Global.styles.js b/ghost/portal/src/components/Global.styles.js index 490e7c6fa8..69eff9c5c2 100644 --- a/ghost/portal/src/components/Global.styles.js +++ b/ghost/portal/src/components/Global.styles.js @@ -39,7 +39,7 @@ export const GlobalStyles = ` line-height: 1.6em; font-weight: 400; font-style: normal; - color: var(--grey4); + color: var(--grey2); box-sizing: border-box; overflow: hidden; } @@ -60,26 +60,26 @@ export const GlobalStyles = ` } h1 { - font-size: 31px; - font-weight: 500; - letter-spacing: 0.2px; + font-size: 35px; + font-weight: 700; + letter-spacing: -0.022em; } h2 { - font-size: 23px; - font-weight: 500; - letter-spacing: 0.2px; + font-size: 32px; + font-weight: 700; + letter-spacing: -0.021em; } h3 { - font-size: 20px; - font-weight: 500; - letter-spacing: 0.2px; + font-size: 24px; + font-weight: 700; + letter-spacing: -0.019em; } p { font-size: 15px; - line-height: 1.55em; + line-height: 1.5em; margin-bottom: 24px; } @@ -106,6 +106,40 @@ export const GlobalStyles = ` padding: 10px; line-height: 1.5em; } + + @media (max-width: 1440px) { + h1 { + font-size: 32px; + letter-spacing: -0.022em; + } + + h2 { + font-size: 28px; + letter-spacing: -0.021em; + } + + h3 { + font-size: 26px; + letter-spacing: -0.02em; + } + } + + @media (max-width: 480px) { + h1 { + font-size: 30px; + letter-spacing: -0.021em; + } + + h2 { + font-size: 26px; + letter-spacing: -0.02em; + } + + h3 { + font-size: 24px; + letter-spacing: -0.019em; + } + } `; export default GlobalStyles; \ No newline at end of file diff --git a/ghost/portal/src/components/Notification.styles.js b/ghost/portal/src/components/Notification.styles.js index 2c31ce8ffe..456a980b63 100644 --- a/ghost/portal/src/components/Notification.styles.js +++ b/ghost/portal/src/components/Notification.styles.js @@ -23,7 +23,7 @@ const NotificationStyles = ` background: rgba(33,33,33,0.95); backdrop-filter: blur(8px); color: var(--white); - border-radius: 5px; + border-radius: 7px; box-shadow: 0 3.2px 3.6px rgba(0, 0, 0, 0.024), 0 8.8px 10px -5px rgba(0, 0, 0, 0.08); animation: notification-slidein 0.55s cubic-bezier(0.215, 0.610, 0.355, 1.000); } diff --git a/ghost/portal/src/components/PopupModal.js b/ghost/portal/src/components/PopupModal.js index cc3933cb18..7c176f1fef 100644 --- a/ghost/portal/src/components/PopupModal.js +++ b/ghost/portal/src/components/PopupModal.js @@ -4,8 +4,8 @@ import AppContext from '../AppContext'; import {getFrameStyles} from './Frame.styles'; import Pages, {getActivePage} from '../pages'; import PopupNotification from './common/PopupNotification'; -import {hasMultipleProducts, isCookiesDisabled, getSitePrices, isInviteOnlySite} from '../utils/helpers'; -import {ReactComponent as GhostLogo} from '../images/ghost-logo-small.svg'; +import PoweredBy from './common/PoweredBy'; +import {getSiteProducts, isInviteOnlySite, isCookiesDisabled, hasFreeProductPrice} from '../utils/helpers'; const React = require('react'); @@ -126,8 +126,9 @@ class PopupContent extends React.Component { } render() { - const {page, site, pageQuery, customSiteUrl} = this.context; - const {is_stripe_configured: isStripeConfigured} = site; + const {page, pageQuery, site, customSiteUrl} = this.context; + const products = getSiteProducts({site}); + const noOfProducts = products.length; getActivePage({page}); const Styles = StylesWrapper({page}); @@ -135,20 +136,8 @@ class PopupContent extends React.Component { ...Styles.page[page] }; let popupWidthStyle = ''; + let popupSize = 'regular'; - const portalPlans = getSitePrices({site, pageQuery}); - - if (page === 'signup' || page === 'signin' || page === 'offer') { - if (!isInviteOnlySite({site, pageQuery}) && portalPlans.length === 3 && (page === 'signup' || page === 'signin')) { - popupWidthStyle = ' gh-portal-container-wide'; - } - if (portalPlans.length <= 1 || !isStripeConfigured) { - popupWidthStyle = 'gh-portal-container-narrow'; - } - if (page === 'offer') { - popupWidthStyle = ' gh-portal-container-wide'; - } - } let cookieBannerText = ''; let pageClass = page; switch (page) { @@ -173,8 +162,19 @@ class PopupContent extends React.Component { break; } - if (hasMultipleProducts({site}) && (page === 'signup' || page === 'signin')) { - pageClass += ' multiple-products'; + if (noOfProducts > 1 && !isInviteOnlySite({site, pageQuery})) { + if (page === 'signup') { + pageClass += ' full-size'; + popupSize = 'full'; + } + } + + const freeProduct = hasFreeProductPrice({site}); + if ((freeProduct && noOfProducts > 2) || (!freeProduct && noOfProducts > 1)) { + if (page === 'accountPlan') { + pageClass += ' full-size'; + popupSize = 'full'; + } } let className = 'gh-portal-popup-container'; @@ -199,15 +199,15 @@ class PopupContent extends React.Component { {this.renderPopupNotification()} {this.renderActivePage()} + {(popupSize === 'full' ? +
+ +
+ : '')}
- { - window.open('https://ghost.org', '_blank'); - }}> - - Powered by Ghost - +
); diff --git a/ghost/portal/src/components/common/ActionButton.js b/ghost/portal/src/components/common/ActionButton.js index 12a31a67d9..f7585ba07c 100644 --- a/ghost/portal/src/components/common/ActionButton.js +++ b/ghost/portal/src/components/common/ActionButton.js @@ -6,7 +6,6 @@ export const ActionButtonStyles = ` .gh-portal-btn-main { box-shadow: none; position: relative; - height: 42px; border: none; } diff --git a/ghost/portal/src/components/common/BackButton.js b/ghost/portal/src/components/common/BackButton.js index 90f3d38a9a..c72f7ce64d 100644 --- a/ghost/portal/src/components/common/BackButton.js +++ b/ghost/portal/src/components/common/BackButton.js @@ -8,15 +8,16 @@ export const BackButtonStyles = ` position: relative; height: unset; min-width: unset; - position: absolute; - top: -3px; - left: -16px; + position: fixed; + top: 29px; + left: 25px; background: none; padding: 8px; margin: 0; box-shadow: none; color: var(--grey3); border: none; + z-index: 10000; } .gh-portal-btn-back:hover { diff --git a/ghost/portal/src/components/common/InputField.js b/ghost/portal/src/components/common/InputField.js index 2018abe8a7..8c9ece1308 100644 --- a/ghost/portal/src/components/common/InputField.js +++ b/ghost/portal/src/components/common/InputField.js @@ -14,14 +14,13 @@ export const InputFieldStyles = ` color: inherit; background: transparent; outline: none; - border: 1px solid var(--grey12); - border-radius: 3px; + border: 1px solid var(--grey11); + border-radius: 6px; width: 100%; - height: 40px; + height: 44px; padding: 0 12px; margin-bottom: 18px; letter-spacing: 0.2px; - box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.07), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.05); transition: border-color 0.25s ease-in-out; } @@ -44,7 +43,7 @@ export const InputFieldStyles = ` } .gh-portal-input:focus { - border-color: #cdcdcd; + border-color: var(--grey8); } .gh-portal-input.error { @@ -52,7 +51,7 @@ export const InputFieldStyles = ` } .gh-portal-input::placeholder { - color: var(--grey7); + color: var(--grey8); } .gh-portal-popup-container:not(.preview) .gh-portal-input:disabled { diff --git a/ghost/portal/src/components/common/PlansSection.js b/ghost/portal/src/components/common/PlansSection.js index ef3bd665aa..00030300ea 100644 --- a/ghost/portal/src/components/common/PlansSection.js +++ b/ghost/portal/src/components/common/PlansSection.js @@ -1,8 +1,7 @@ import React, {useContext} from 'react'; import AppContext from '../../AppContext'; -import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg'; import calculateDiscount from '../../utils/discount'; -import {isCookiesDisabled, formatNumber, hasOnlyFreePlan, hasMultipleProductsFeature, getProductBenefits, getFreeTierDescription, getFreeTierTitle, getFreeProductBenefits, getProductFromPrice} from '../../utils/helpers'; +import {isCookiesDisabled, formatNumber, hasOnlyFreePlan} from '../../utils/helpers'; import ProductsSection, {ChangeProductSection} from './ProductsSection'; export const PlanSectionStyles = ` @@ -64,25 +63,21 @@ export const PlanSectionStyles = ` border-right: none; } - .gh-portal-plans-container:not(.has-multiple-products) { - margin-bottom: 2px; - } - - .gh-portal-plans-container.has-multiple-products:not(.empty-selected-benefits) .gh-portal-plan-section::before { + .gh-portal-plans-container:not(.empty-selected-benefits) .gh-portal-plan-section::before { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } - .gh-portal-plans-container.has-multiple-products.has-discount { + .gh-portal-plans-container.has-discount { margin-top: 40px; } - .gh-portal-plans-container.has-multiple-products.has-discount, - .gh-portal-plans-container.has-multiple-products.has-discount .gh-portal-plan-section:last-of-type::before { + .gh-portal-plans-container.has-discount, + .gh-portal-plans-container.has-discount .gh-portal-plan-section:last-of-type::before { border-top-right-radius: 0; } - .gh-portal-plans-container.is-change-plan.has-multiple-products .gh-portal-plan-section::before { + .gh-portal-plans-container.is-change-plan .gh-portal-plan-section::before { border-top-left-radius: 0; border-top-right-radius: 0; } @@ -98,7 +93,7 @@ export const PlanSectionStyles = ` margin-top: 2px; } - .gh-portal-plans-container.has-multiple-products .gh-portal-plan-pricelabel { + .gh-portal-plans-container .gh-portal-plan-pricelabel { min-height: unset; } @@ -167,75 +162,10 @@ export const PlanSectionStyles = ` word-break: break-word; } - .gh-portal-plan-checkbox { - position: relative; - display: block; - font-size: 22px; - height: 18px; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - - .gh-portal-plans-container.disabled .gh-portal-plan-checkbox { - cursor: auto; - } - - .gh-portal-plan-checkbox input { - position: absolute; - height: 0; - width: 0; - opacity: 0; - cursor: pointer; - } - - .gh-portal-plan-checkbox .checkmark { - position: absolute; - top: 0; - left: -9px; - background-color: var(--grey12); - border-radius: 999px; - height: 18px; - width: 18px; - } - - .gh-portal-plan-checkbox input:checked ~ .checkmark { - background-color: var(--brandcolor); - } - - .gh-portal-plan-checkbox .checkmark::after { - position: absolute; - display: none; - content: ""; - } - - .gh-portal-plan-checkbox input:checked ~ .checkmark:after { - display: block; - } - - .gh-portal-plan-checkbox .checkmark:after { - left: 6.5px; - top: 2.5px; - width: 5px; - height: 11px; - border: solid var(--white); - border-width: 0 2px 2px 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - } - - .gh-portal-plans-container.disabled .gh-portal-plan-checkbox input:checked ~ .checkmark { - opacity: 0.3; - } - .gh-portal-content.signup.singleplan .gh-portal-plan-section { cursor: auto; } - .gh-portal-content.signup.singleplan .gh-portal-plan-checkbox, .gh-portal-content.signup.singleplan .gh-portal-plan-section.checked::before { display: none; } @@ -263,10 +193,6 @@ export const PlanSectionStyles = ` opacity: 0; } - .gh-portal-plans-container.hide-checkbox .gh-portal-plan-checkbox { - display: none; - } - .gh-portal-plans-container.hide-checkbox .gh-portal-plan-section { padding-top: 12px; padding-bottom: 12px; @@ -281,107 +207,17 @@ export const PlanSectionStyles = ` margin: 3px 0 -2px; } - .gh-portal-plans-container.vertical { - flex-direction: column; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-section { - display: grid; - flex-direction: unset; - grid-template-columns: 32px auto auto; - grid-template-rows: auto auto; - justify-items: start; - min-height: 60px; - border-right: none; - border-bottom: 1px solid var(--grey11); - padding: 10px; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-checkbox { - grid-column: 1 / 2; - grid-row: 1 / 3; - margin: 0 12px; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-pricelabel { - grid-column: 3 / 4; - grid-row: 1 / 3; - flex-direction: column; - justify-self: end; - align-items: flex-end; - margin: 4px 4px 0 12px; - min-height: unset; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-priceinterval { - line-height: unset; - line-height: 1.7; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-name { - text-transform: none; - font-size: 1.4rem; - line-height: 1.1em; - letter-spacing: 0.2px; - margin: 0; - min-height: unset; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-featurewrapper { - margin: 4px 0 0; - padding: 0; - border: none; - align-items: flex-start; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-feature { - text-align: left; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-section:last-of-type { - border-bottom: none; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-section:first-of-type::before { - border-radius: 5px 5px 0 0; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-section:last-of-type::before { - border-radius: 0 0 5px 5px; - } - - .gh-portal-plans-container.vertical.hide-checkbox .gh-portal-plan-section { - grid-template-columns: auto auto; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-pricelabel { - grid-column: 3 / 4; - grid-row: 1 / 3; - } - - .gh-portal-plans-container.vertical.hide-checkbox .gh-portal-plan-featurewrapper { - grid-column: 1 / 2; - } - - .gh-portal-plans-container.vertical .gh-portal-plan-name.no-description { - grid-row: 1 / 3; - } - - .gh-portal-plans-container.multiple-products { - border: none; - } - - .gh-portal-plans-container.has-multiple-products:not(.empty-selected-benefits) { + .gh-portal-plans-container:not(.empty-selected-benefits) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } - .gh-portal-plans-container.has-multiple-products.is-change-plan { + .gh-portal-plans-container.is-change-plan { border-radius: 0 0 5px 5px; border-top: none; } - .gh-portal-plans-container.has-multiple-products.is-change-plan .gh-portal-plan-section { + .gh-portal-plans-container.is-change-plan .gh-portal-plan-section { min-height: 90px; } @@ -447,26 +283,6 @@ export const PlanSectionStyles = ` } `; -function Checkbox({name, id, onPlanSelect, isChecked, disabled = false}) { - if (isCookiesDisabled()) { - disabled = true; - } - return ( -
- onPlanSelect(e, id)} - disabled={disabled} - /> - -
- ); -} - function PriceLabel({currencySymbol, price, interval}) { const isSymbol = currencySymbol.length !== 3; const currencyClass = isSymbol ? 'gh-portal-plan-currency gh-portal-plan-currency-symbol' : 'gh-portal-plan-currency gh-portal-plan-currency-code'; @@ -495,175 +311,7 @@ function addDiscountToPlans(plans) { } } -function PlanOptions({plans, selectedPlan, onPlanSelect, changePlan}) { - const {site} = useContext(AppContext); - addDiscountToPlans(plans); - - return plans.map(({name, type, currency_symbol: currencySymbol, amount, description, interval, id}) => { - const price = amount / 100; - const isChecked = selectedPlan === id; - const planDetails = {}; - let displayName = name; - if (type === 'free') { - displayName = getFreeTierTitle({site}); - planDetails.feature = 'Free preview'; - } else { - displayName = interval === 'month' ? 'Monthly' : 'Yearly'; - planDetails.feature = description || 'Full access'; - } - // switch (name) { - // case 'Free': - // displayName = getFreeTierTitle({site}); - // planDetails.feature = 'Free preview'; - // break; - // default: - // displayName = interval === 'month' ? 'Monthly' : 'Yearly'; - // planDetails.feature = description || 'Full access'; - // break; - // } - - let planClass = isChecked ? 'gh-portal-plan-section checked' : 'gh-portal-plan-section'; - const planNameClass = planDetails.feature ? 'gh-portal-plan-name' : 'gh-portal-plan-name no-description'; - const featureClass = hasMultipleProductsFeature({site}) ? 'gh-portal-plan-featurewrapper hidden' : 'gh-portal-plan-featurewrapper'; - - return ( -
onPlanSelect(e, id)}> - {(hasMultipleProductsFeature({site}) ? : ``)} - -

{displayName}

- -
- - {(changePlan && selectedPlan === id ? Current plan : '')} -
-
- ); - }); -} - -function PlanDiscount({discount}) { - return ( -
{discount}
- ); -} - -function PlanFeature({feature, hide = false}) { - if (hide) { - return null; - } - return ( -
- {feature} -
- ); -} - -function PlanBenefit({benefit}) { - if (!benefit?.name) { - return null; - } - return ( -
- - {benefit.name} -
- ); -} - -function PlanBenefits({product, plans, selectedPlan}) { - const {site} = useContext(AppContext); - const productBenefits = getProductBenefits({product, site}); - const plan = plans.find((_plan) => { - return _plan.id === selectedPlan; - }); - let planBenefits = []; - let planDescription = product?.description || ''; - if (selectedPlan === 'free') { - planBenefits = getFreeProductBenefits({site}); - planDescription = getFreeTierDescription({site}); - } else if (plan?.interval === 'month') { - planBenefits = productBenefits.monthly; - } else if (plan?.interval === 'year') { - planBenefits = productBenefits.yearly; - } - const benefits = planBenefits.map((benefit, idx) => { - const key = `${benefit.name}-${idx}`; - return ( - - ); - }); - - if (!planDescription && benefits.length === 0) { - return ''; - } - - let benefitsClass = benefits.length === 0 ? `no-benefits` : ''; - if (!product || hasOnlyFreePlan({plans})) { - benefitsClass += ' onlyfree'; - } - - return ( -
- {planDescription ?
{planDescription}
: ''} - {benefits} -
- ); -} - -function PlanLabel({showLabel}) { - if (!showLabel) { - return null; - } - return ( - - ); -} - -function productHasDescriptionOrBenefits({product}) { - if (product?.description || product?.benefits?.length) { - return true; - } - return false; -} - -function getPlanClassNames({changePlan, cookiesDisabled, plans = [], selectedPlan, showVertical = false, site}) { - let className = 'gh-portal-plans-container'; - if (changePlan) { - className += ' hide-checkbox'; - } - if (cookiesDisabled) { - className += ' disabled'; - } - if (changePlan || plans.length > 3 || showVertical) { - className += ' vertical'; - } - if (hasMultipleProductsFeature({site})) { - className += ' has-multiple-products'; - const selectedProduct = getProductFromPrice({site, priceId: selectedPlan}); - - if (!productHasDescriptionOrBenefits({product: selectedProduct})) { - className += ' empty-selected-benefits'; - } - - const filteredPlans = plans.filter(d => d.id !== 'free'); - const monthlyPlan = plans.find((d) => { - return d.name === 'Monthly' && !d.description && d.interval === 'month'; - }); - const yearlyPlan = plans.find((d) => { - return d.name === 'Yearly' && !d.description && d.interval === 'year'; - }); - - if (filteredPlans.length === 2 && monthlyPlan && yearlyPlan) { - const discount = calculateDiscount(monthlyPlan.amount, yearlyPlan.amount); - if (discount) { - className += ' has-discount'; - } - } - } - return className; -} - -export function MultipleProductsPlansSection({products, selectedPlan, onPlanSelect, changePlan = false}) { +export function MultipleProductsPlansSection({products, selectedPlan, onPlanSelect, onPlanCheckout, changePlan = false}) { const cookiesDisabled = isCookiesDisabled(); /**Don't allow plans selection if cookies are disabled */ if (cookiesDisabled) { @@ -692,48 +340,21 @@ export function MultipleProductsPlansSection({products, selectedPlan, onPlanSele type='upgrade' products={products} onPlanSelect={onPlanSelect} + handleChooseSignup={(...args) => { + onPlanCheckout(...args); + }} /> ); } -export function SingleProductPlansSection({product, plans, selectedPlan, onPlanSelect, changePlan = false}) { - const {site} = useContext(AppContext); - const cookiesDisabled = isCookiesDisabled(); - /**Don't allow plans selection if cookies are disabled */ - if (cookiesDisabled) { - onPlanSelect = () => {}; - } - const className = getPlanClassNames({cookiesDisabled, changePlan, plans, selectedPlan, site}); - - if (!product || hasOnlyFreePlan({plans})) { - return ( -
- -
- ); - } - - return ( -
-
- -
- -
- ); -} - function getChangePlanClassNames({cookiesDisabled, site}) { let className = 'gh-portal-plans-container is-change-plan hide-checkbox'; if (cookiesDisabled) { className += ' disabled'; } - if (hasMultipleProductsFeature({site})) { - className += ' has-multiple-products'; - } return className; } @@ -783,26 +404,3 @@ export function ChangeProductPlansSection({product, plans, selectedPlan, onPlanS ); } - -function PlansSection({plans, showLabel = true, selectedPlan, onPlanSelect, changePlan = false}) { - const {site} = useContext(AppContext); - if (hasOnlyFreePlan({plans})) { - return null; - } - const cookiesDisabled = isCookiesDisabled(); - /**Don't allow plans selection if cookies are disabled */ - if (cookiesDisabled) { - onPlanSelect = () => {}; - } - const className = getPlanClassNames({cookiesDisabled, changePlan, plans, selectedPlan, site}); - return ( -
- -
- -
-
- ); -} - -export default PlansSection; diff --git a/ghost/portal/src/components/common/PlansSection.test.js b/ghost/portal/src/components/common/PlansSection.test.js deleted file mode 100644 index 2fef536227..0000000000 --- a/ghost/portal/src/components/common/PlansSection.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import {render} from '@testing-library/react'; -import PlansSection from './PlansSection'; - -const setup = (overrides = {}) => { - const mockOnPlanSelectFn = jest.fn(); - const props = { - plans: [ - {type: 'free', price: 'Decide later', currency_symbol: '$', name: 'Free', id: 'free'}, - {type: 'month', price: 12, currency_symbol: '$', name: 'Monthly', id: 'monthly'}, - {type: 'year', price: 110, currency_symbol: '$', name: 'Yearly', id: 'yearly'} - ], - selectedPlan: 'Monthly', - onPlanSelect: mockOnPlanSelectFn - }; - const utils = render( - - ); - - const freeCheckboxEl = utils.getByLabelText('Free'); - const monthlyCheckboxEl = utils.getByLabelText('Monthly'); - const yearlyCheckboxEl = utils.getByLabelText('Yearly'); - return { - freeCheckboxEl, - monthlyCheckboxEl, - yearlyCheckboxEl, - mockOnPlanSelectFn, - ...utils - }; -}; - -describe('InputField', () => { - test('renders', () => { - const {freeCheckboxEl, monthlyCheckboxEl, yearlyCheckboxEl} = setup(); - expect(freeCheckboxEl).toBeInTheDocument(); - expect(monthlyCheckboxEl).toBeInTheDocument(); - expect(yearlyCheckboxEl).toBeInTheDocument(); - }); -}); diff --git a/ghost/portal/src/components/common/PopupNotification.js b/ghost/portal/src/components/common/PopupNotification.js index 863d301faa..85b32fc7ff 100644 --- a/ghost/portal/src/components/common/PopupNotification.js +++ b/ghost/portal/src/components/common/PopupNotification.js @@ -12,16 +12,16 @@ export const PopupNotificationStyles = ` top: 8px; left: 8px; right: 8px; - padding: 8px; + padding: 12px; background: var(--grey2); - z-index: 9999; - border-radius: 4px; - font-size: 1.3rem; + z-index: 11000; + border-radius: 5px; + font-size: 1.5rem; box-shadow: 0px 0.8151839971542358px 0.8151839971542358px 0px rgba(0,0,0,0.01), 0px 2.2538793087005615px 2.2538793087005615px 0px rgba(0,0,0,0.02), 0px 5.426473140716553px 5.426473140716553px 0px rgba(0,0,0,0.03), 0px 18px 18px 0px rgba(0,0,0,0.04); - animation: popupnotification-slidein 0.6s ease-in-out; + animation: popupnotification-slidein 0.3s ease-in-out; } .gh-portal-popupnotification.slideout { @@ -32,7 +32,7 @@ export const PopupNotificationStyles = ` color: var(--white); margin: 0; padding: 0 20px; - font-size: 1.4rem; + font-size: 1.5rem; line-height: 1.5em; letter-spacing: 0.2px; text-align: center; @@ -44,10 +44,10 @@ export const PopupNotificationStyles = ` .gh-portal-popupnotification-icon { position: absolute; - top: 10px; - left: 10px; - width: 16px; - height: 16px; + top: 12px; + left: 12px; + width: 20px; + height: 20px; } .gh-portal-popupnotification-icon.success { @@ -60,15 +60,15 @@ export const PopupNotificationStyles = ` .gh-portal-popupnotification .closeicon { position: absolute; - top: 0px; + top: 3px; bottom: 0; - right: 0; + right: 3px; color: var(--white); cursor: pointer; - width: 12px; - height: 12px; + width: 16px; + height: 16px; padding: 12px; - transition: all 0.2s ease-in-out forwards; + transition: all 0.15s ease-in-out forwards; opacity: 0.8; } @@ -77,15 +77,27 @@ export const PopupNotificationStyles = ` } @keyframes popupnotification-slidein { - 0% { transform: translateY(-100px); } - 60% { transform: translateY(8px); } - 100% { transform: translateY(0); } + 0% { + transform: translateY(-10px); + opacity: 0; + } + 60% { transform: translateY(2px); } + 100% { + transform: translateY(0); + opacity: 1.0; + } } @keyframes popupnotification-slideout { - 0% { transform: translateY(0); } - 40% { transform: translateY(8px); } - 100% { transform: translateY(-100px); } + 0% { + transform: translateY(0); + opacity: 1.0; + } + 40% { transform: translateY(2px); } + 100% { + transform: translateY(-10px); + opacity: 0; + } } `; diff --git a/ghost/portal/src/components/common/PoweredBy.js b/ghost/portal/src/components/common/PoweredBy.js new file mode 100644 index 0000000000..2ded25c76b --- /dev/null +++ b/ghost/portal/src/components/common/PoweredBy.js @@ -0,0 +1,15 @@ +import React from 'react'; +import {ReactComponent as GhostLogo} from '../../images/ghost-logo-small.svg'; + +export default class PoweredBy extends React.Component { + render() { + return ( + { + window.open('https://ghost.org', '_blank'); + }}> + + Powered by Ghost + + ); + } +} \ No newline at end of file diff --git a/ghost/portal/src/components/common/ProductsSection.js b/ghost/portal/src/components/common/ProductsSection.js index f541a1b977..94db7a7ce4 100644 --- a/ghost/portal/src/components/common/ProductsSection.js +++ b/ghost/portal/src/components/common/ProductsSection.js @@ -1,35 +1,72 @@ import React, {useContext, useEffect, useState} from 'react'; -import Switch from '../common/Switch'; +import {ReactComponent as LoaderIcon} from '../../images/icons/loader.svg'; import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg'; -import {getSiteProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct} from '../../utils/helpers'; +import {getCurrencySymbol, getPriceString, getStripeAmount, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct, getFreeProductBenefits, formatNumber, isCookiesDisabled, hasOnlyFreeProduct} from '../../utils/helpers'; import AppContext from '../../AppContext'; -import ActionButton from './ActionButton'; import calculateDiscount from '../../utils/discount'; export const ProductsSectionStyles = ({site}) => { - const products = getSiteProducts({site}); - const noOfProducts = products.length; + // const products = getSiteProducts({site}); + // const noOfProducts = products.length; return ` .gh-portal-products { display: flex; flex-direction: column; align-items: center; - background: var(--grey13); - margin: 40px -32px; - padding: 0 32px; } - .gh-portal-products-priceswitch { + .gh-portal-products-pricetoggle { + position: relative; display: flex; - justify-content: center; - padding-top: 32px; + background: #F3F3F3; + width: 100%; + border-radius: 999px; + padding: 4px; + height: 44px; + margin: 0 0 40px; + } + + .gh-portal-products-pricetoggle:before { + position: absolute; + content: ""; + display: block; + width: 50%; + top: 4px; + bottom: 4px; + right: 4px; + background: #fff; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.08); + border-radius: 999px; + transition: all 0.15s ease-in-out; + } + + .gh-portal-products-pricetoggle.left:before { + transform: translateX(calc(-100% + 8px)); + } + + .gh-portal-products-pricetoggle .gh-portal-btn { + border: 0; + height: 100% !important; + width: 50%; + border-radius: 999px; + color: var(--grey7); + background: transparent; + font-size: 1.5rem; + } + + .gh-portal-products-pricetoggle .gh-portal-btn.active { + border: 0; + height: 100%; + width: 50%; + color: var(--grey0); + /*background: white; + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.08);*/ } .gh-portal-priceoption-label { - font-size: 1.3rem; - font-weight: 600; + font-size: 1.4rem; + font-weight: 400; letter-spacing: 0.3px; - text-transform: uppercase; margin: 0 6px; min-width: 180px; } @@ -38,363 +75,271 @@ export const ProductsSectionStyles = ({site}) => { text-align: right; } - .gh-portal-products-priceswitch .gh-portal-for-switch label, .gh-portal-for-switch .container { - width: 43px !important; - } - - .gh-portal-products-priceswitch .gh-portal-for-switch .input-toggle-component, - .gh-portal-products-priceswitch .gh-portal-for-switch label:hover input:not(:checked) + .input-toggle-component, - .gh-portal-products-priceswitch .gh-portal-for-switch .container:hover input:not(:checked) + .input-toggle-component { - background: var(--grey1); - border-color: var(--grey1); - box-shadow: none; - width: 43px !important; - height: 24px !important; - } - - .gh-portal-products-priceswitch .gh-portal-for-switch .input-toggle-component:before { - height: 18px !important; - width: 18px !important; - } - - .gh-portal-products-priceswitch .gh-portal-for-switch input:checked + .input-toggle-component { - background: var(--grey1); - } - - .gh-portal-products-priceswitch .gh-portal-for-switch input:checked + .input-toggle-component:before { - transform: translateX(19px); - } - - .gh-portal-discount-label { - position: absolute; - top: -19px; - left: -1px; - right: -1px; - color: var(--brandcolor); - font-size: 1.1rem; - font-weight: 600; - letter-spacing: 0.3px; - text-transform: uppercase; - padding: 0px 4px; - text-align: center; - white-space: nowrap; - } - - .gh-portal-discount-label:before { - position: absolute; - content: ""; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: var(--brandcolor); - opacity: 0.15; - border-radius: 2px 2px 0 0; - } - - .gh-portal-products-priceswitch .gh-portal-discount-label { - position: relative; - font-size: 1.25rem; - letter-spacing: 0.25px; - margin: 0 0 0 6px; - padding: 2px 6px; - top: unset; - left: unset; - right: unset; - width: unset; - } - - .gh-portal-products-priceswitch .gh-portal-discount-label:before { - border-radius: 3px; + .gh-portal-priceoption-label.inactive { + color: var(--grey8); } .gh-portal-products-grid { - display: grid; - grid-template-columns: repeat(${productColumns(noOfProducts)}, minmax(0, ${(productColumns(noOfProducts) <= 3 ? `360px` : `300px`)})); - grid-gap: 32px; + display: flex; + flex-wrap: wrap; + align-items: stretch; + justify-content: center; + gap: 40px; margin: 0 auto; - padding: 32px 2vw; + padding: 0; + width: 100%; } - @media (max-width: 1280px) { - .gh-portal-products-grid { - grid-template-columns: repeat(${((productColumns(noOfProducts) >= 3) ? 3 : productColumns(noOfProducts))}, minmax(0, 300px)); - } - } - - @media (max-width: 960px) { - .gh-portal-products-grid { - grid-template-columns: repeat(${((productColumns(noOfProducts) >= 2) ? 2 : productColumns(noOfProducts))}, minmax(0, 300px)); - } - } - - @media (max-width: 360px) { - div:not(.gh-portal-products-priceswitch) .gh-portal-discount-label { - font-size: 0.9rem; - white-space: nowrap; - } - } - - .gh-portal-product-card { + flex: 1; + max-width: 420px; + min-width: 320px; position: relative; display: flex; flex-direction: column; - align-items: center; - justify-content: space-between; + align-items: flex-start; + justify-content: stretch; background: white; - padding: 24px; - border-radius: 3px; - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); - min-height: 240px; - cursor: pointer; + padding: 32px; + border-radius: 7px; + border: 1px solid var(--grey11); + min-height: 200px; + transition: border-color 0.25s ease-in-out; + } + + .gh-portal-product-card.top { + border-bottom: none; + border-radius: 7px 7px 0 0; + padding-bottom: 0; + } + + .gh-portal-product-card.bottom { + border-top: none; + border-radius: 0 0 7px 7px; + padding-top: 0; + } + + .gh-portal-product-card:not(.disabled):hover { + border-color: var(--grey9); } .gh-portal-product-card.checked::before { position: absolute; display: block; - top: -1px; - right: -1px; - bottom: -1px; - left: -1px; + top: -2px; + right: -2px; + bottom: -2px; + left: -2px; content: ""; z-index: 999; - border: 2px solid var(--brandcolor); + border: 0px solid var(--brandcolor); pointer-events: none; - border-radius: 3px; - } - - .gh-portal-product-card.checked { - box-shadow: none; + border-radius: 7px; } .gh-portal-product-card-header { + width: 100%; + min-height: 56px; + } + + .gh-portal-product-card-details { + flex: 1; display: flex; flex-direction: column; - align-items: center; width: 100%; } .gh-portal-product-name { - font-size: 1.3rem; - font-weight: 500; - line-height: 1.45em; - letter-spacing: 0.5px; - text-transform: uppercase; - margin-top: 7px; - text-align: center; - min-height: 24px; + font-size: 1.8rem; + font-weight: 600; + line-height: 1.3em; + letter-spacing: 0px; + margin-top: -4px; word-break: break-word; width: 100%; - color: var(--grey1); - border-bottom: 1px solid var(--grey12); - padding: 8px 0 16px; - margin-bottom: 12px; + color: var(--brandcolor); } - .gh-portal-product-description { - font-size: 1.35rem; - font-weight: 500; - line-height: 1.45em; + .gh-portal-discount-label { + position: relative; + font-size: 1.25rem; + line-height: 1em; + font-weight: 600; + letter-spacing: 0.3px; + color: var(--grey0); + padding: 6px 9px; text-align: center; - color: var(--grey3); - margin-bottom: 14px; - margin-top: 8px; - padding: 0 4px; - width: 100%; + white-space: nowrap; + border-radius: 999px; + margin-right: -4px; + max-height: 24.5px; } - .gh-portal-product-card .gh-portal-product-description { - padding-bottom: 20px; - margin-bottom: 16px; + .gh-portal-discount-label:before { + position: absolute; + content: ""; + display: block; + background: var(--brandcolor); + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: 999px; + opacity: 0.2; + } + + .gh-portal-product-card-pricecontainer { + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: space-between; + flex-wrap: wrap; + row-gap: 10px; + column-gap: 4px; + width: 100%; + margin-top: 16px; } .gh-portal-product-price { display: flex; justify-content: center; + color: var(--grey0); } .gh-portal-product-price .currency-sign { align-self: flex-start; - font-size: 2.0rem; - font-weight: 500; - line-height: 1.3em; + font-size: 2.7rem; + font-weight: 700; + line-height: 1.115em; + } + + .gh-portal-product-price .currency-sign.long { + margin-right: 5px; } .gh-portal-product-price .amount { - font-size: 3.3rem; - font-weight: 500; + font-size: 3.5rem; + font-weight: 700; line-height: 1em; - color: var(--grey2); + letter-spacing: -1.3px; + color: var(--grey0); } .gh-portal-product-price .billing-period { align-self: flex-end; - font-size: 1.3rem; + font-size: 1.5rem; line-height: 1.6em; - color: var(--grey4); + color: var(--grey5); letter-spacing: 0.3px; - margin-left: 2px; + margin-left: 5px; } .gh-portal-product-alternative-price { - font-size: 1.2rem; + font-size: 1.3rem; line-height: 1.6em; color: var(--grey8); - text-align: center; - margin-top: 4px; letter-spacing: 0.3px; - height: 18px; + display: none; + } + + .gh-portal-product-card-detaildata { + flex: 1; + } + + .gh-portal-product-description { + font-size: 1.55rem; + font-weight: 600; + line-height: 1.4em; + width: 100%; + margin-top: 16px; } .gh-portal-product-benefits { - font-size: 1.3rem; - line-height: 1.45em; - margin: -8px 0 0; - padding: 16px 8px 16px; - color: var(--grey3); + font-size: 1.5rem; + line-height: 1.4em; width: 100%; - } - - .gh-portal-product-card .gh-portal-product-description + .gh-portal-product-benefits { - border-top: 1px solid var(--grey12); - margin-top: -16px; - padding-top: 24px; + margin-top: 16px; } .gh-portal-product-benefit { display: flex; align-items: flex-start; - margin-bottom: 12px; + margin-bottom: 10px; } .gh-portal-benefit-checkmark { - width: 13px; - height: 13px; - min-width: 12px; - margin: 3px 6px 0 0; + width: 14px; + height: 14px; + min-width: 14px; + margin: 3px 10px 0 0; overflow: visible; } - .gh-portal-benefit-checkmark polyline { + .gh-portal-benefit-checkmark polyline, + .gh-portal-benefit-checkmark g { stroke-width: 3px; } - .gh-portal-benefit-title { - flex-grow: 1; - color: var(--grey5); - } - .gh-portal-products-grid.change-plan { padding: 0; } - /* Vertical card style - for smaller screens sizes*/ - .gh-portal-product-card.vertical { - display: none !important; - grid-template-columns: 16px minmax(0, 1fr) auto; - column-gap: 12px; - row-gap: 8px; + .gh-portal-btn-product { + position: sticky; + bottom: 0; + display: flex; + flex-direction: row; + align-items: flex-end; + width: 100%; + justify-self: flex-end; + padding: 40px 0 32px; + margin-bottom: -32px; + background: rgb(255,255,255); + background: linear-gradient(0deg, rgba(255,255,255,1) 75%, rgba(255,255,255,0) 100%); + } + + .gh-portal-btn-product .gh-portal-btn { + background: var(--brandcolor); + color: #fff; + border: none; + width: 100%; + } + + .gh-portal-btn-product .gh-portal-btn:hover { + opacity: 0.9; + } + + .gh-portal-current-plan { + display: flex; align-items: center; - min-height: 68px; - padding: 12px 20px; - justify-content: unset; - } - - .gh-portal-product-card.vertical .gh-portal-plan-checkbox { - grid-row: 1; - grid-column: 1 / 2; - height: unset; - } - - .gh-portal-product-card.vertical .gh-portal-plan-checkbox .checkmark { - top: -9px; - left: -5px; - } - - .gh-portal-product-card.vertical .gh-portal-product-name { - grid-row: 1; - grid-column: 2 / 3; - font-size: 1.7rem; - letter-spacing: 0.3px; - line-height: 1.3em; + font-size: 1.5rem; font-weight: 500; - text-transform: none; - text-align: left; - margin: 4px 12px 4px 0; - padding: 0; - border-bottom: none; + line-height: 1em; + letter-spacing: 0.2px; + text-align: center; + white-space: nowrap; + width: 100%; + height: 44px; + border-radius: 5px; + color: var(--grey0); + font-weight: 600; + } + + .gh-portal-product-card.only-free { + margin: 0 0 16px; min-height: unset; } - .gh-portal-product-card.vertical .gh-portal-product-pricecontainer { - grid-row: 1; - grid-column: 3 / 4; - } - - .gh-portal-product-card.vertical .gh-portal-product-price { - justify-content: flex-end; - margin-top: -1px; - } - - .gh-portal-product-card.vertical .gh-portal-product-price .amount { - font-size: 2.6rem !important; - } - - .gh-portal-product-card.vertical .gh-portal-product-price .billing-period { - line-height: 1.3em; - font-size: 1.1rem; - } - - .gh-portal-product-card.vertical .gh-portal-product-alternative-price { - display: none; - text-align: right; - margin-top: 1px; - font-size: 1.1rem; - } - - .gh-portal-product-card.vertical .gh-portal-product-description { - grid-row: 2; - grid-column: 2 / 4; - margin-bottom: 0px; - padding-top: 12px; - padding-bottom: 8px; - padding-left: 0; - margin-top: 2px; - border-top: 1px solid var(--grey12); - text-align: left; - } - - .gh-portal-product-card.vertical .gh-portal-product-benefits { - grid-row: 3; - grid-column: 2 / 4; - margin: 0; - padding-top: 0; - padding-left: 2px; - } - - .gh-portal-product-card.vertical .gh-portal-product-benefit { - margin-bottom: 8px; - } - - .gh-portal-product-card.vertical .gh-portal-product-description + .gh-portal-product-benefits { - border-top: none; - padding-top: 0; - margin-top: -2px; + .gh-portal-product-card.only-free .gh-portal-product-card-header { + min-height: unset; } @media (max-width: 670px) { - .gh-portal-products { - margin: 24px -32px 0 -32px; - padding: 12px 32px 20px 32px; - } - .gh-portal-products-grid { grid-template-columns: unset; grid-gap: 20px; - padding: 32px 0 0; width: 100%; - max-width: 420px; + max-width: 440px; } .gh-portal-priceoption-label { @@ -409,62 +354,35 @@ export const ProductsSectionStyles = ({site}) => { padding-top: 18px; } - .gh-portal-product-card:not(.vertical) { - display: none !important; - } - - .gh-portal-product-card.vertical { - display: grid !important; + .gh-portal-product-card { + min-height: unset; } .gh-portal-singleproduct-benefits .gh-portal-product-description { text-align: center; } - .gh-portal-product-price .currency-sign { - font-size: 1.5rem; - } - - .gh-portal-product-price .amount { - font-size: 2.6rem; - } - .gh-portal-product-benefit:last-of-type { margin-bottom: 0; } } - @media (max-width: 390px) { - .gh-portal-product-card.vertical { - padding: 12px; - } - - .gh-portal-product-card.vertical .gh-portal-plan-checkbox { - margin: 0 5px; + @media (max-width: 480px) { + .gh-portal-product-price .amount { + font-size: 3.4rem; } } /* Upgrade and change plan*/ .gh-portal-upgrade-product { margin-top: -70px; - margin-bottom: 32px; padding-top: 60px; - padding-bottom: 32px; } .gh-portal-upgrade-product .gh-portal-products-grid { grid-template-columns: unset; grid-gap: 20px; width: 100%; - padding: 32px 0 0; - } - - .gh-portal-upgrade-product .gh-portal-product-card { - display: none !important; - } - - .gh-portal-upgrade-product .gh-portal-product-card.vertical { - display: grid !important; } .gh-portal-upgrade-product .gh-portal-discount-label { @@ -492,6 +410,18 @@ export const ProductsSectionStyles = ({site}) => { background: var(--brandcolor); opacity: 0.15; } + + @media (max-width: 880px) { + .gh-portal-products-grid { + flex-direction: column; + margin: 0 auto; + max-width: 420px; + } + + .gh-portal-product-card-header { + min-height: unset; + } + } `; }; @@ -502,37 +432,6 @@ const ProductsContext = React.createContext({ setSelectedProduct: null }); -function productColumns(noOfProducts) { - return noOfProducts >= 5 ? 5 : noOfProducts; -} - -function Checkbox({name, id, onProductSelect, isChecked, disabled = false}) { - if (isCookiesDisabled()) { - disabled = true; - } - return ( -
- { - onProductSelect(e, id); - }} - aria-label={name} - disabled={disabled} - /> - { - e.stopPropagation(); - if (!disabled) { - onProductSelect(e, id); - } - }}> -
- ); -} - function ProductBenefits({product}) { if (!product.benefits || !product.benefits.length) { return null; @@ -549,15 +448,12 @@ function ProductBenefits({product}) { }); } -function ProductBenefitsContainer({product, showVertical = false, hide = false}) { +function ProductBenefitsContainer({product, hide = false}) { if (!product.benefits || !product.benefits.length || hide) { return null; } let className = 'gh-portal-product-benefits'; - if (showVertical) { - className += ' vertical'; - } return (
@@ -588,126 +484,157 @@ function ProductCardPrice({product}) { if (!monthlyPrice || !yearlyPrice) { return null; } + + const yearlyDiscount = calculateDiscount(product.monthlyPrice.amount, product.yearlyPrice.amount); + const currencySymbol = getCurrencySymbol(activePrice.currency); + return (
- {getCurrencySymbol(activePrice.currency)} - {getStripeAmount(activePrice.amount)} + 1 ? ' long' : '')}>{currencySymbol} + {formatNumber(getStripeAmount(activePrice.amount))} /{activePrice.interval}
+ {(selectedInterval === 'year' ? : '')}
); } -function ProductCard({product}) { +function FreeProductCard({products, handleChooseSignup}) { + const {site, action} = useContext(AppContext); const {selectedProduct, setSelectedProduct} = useContext(ProductsContext); - const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card'; - // Product cards are duplicated because their design is too different for mobile devices to handle it purely in CSS - return ( - <> - {/* Standard, desktop card */} -
{ - e.stopPropagation(); - setSelectedProduct(product.id); - }}> -
- { - setSelectedProduct(product.id); - }} /> -

{product.name}

- {product.description ?
{product.description}
: ''} - -
- -
- - {/* Vertical version */} -
{ - e.stopPropagation(); - setSelectedProduct(product.id); - }}> - { - setSelectedProduct(product.id); - }} /> -

{product.name}

- - {product.description ?
{product.description}
: ''} - -
- - ); -} - -function ProductCards({products}) { - return products.map((product) => { - if (product.id === 'free') { - return ( - - ); - } - return ( - - ); - }); -} - -function FreeProductCard() { - const {site} = useContext(AppContext); - const {selectedProduct, selectedInterval, setSelectedProduct} = useContext(ProductsContext); - - const cardClass = selectedProduct === 'free' ? 'gh-portal-product-card free checked' : 'gh-portal-product-card free'; + let cardClass = selectedProduct === 'free' ? 'gh-portal-product-card free checked' : 'gh-portal-product-card free'; const product = getFreeProduct({site}); const freeProductDescription = getFreeTierDescription({site}); - // Product cards are duplicated because their design is too different for mobile devices to handle it purely in CSS + + let disabled = (action === 'signup:running') ? true : false; + + if (isCookiesDisabled()) { + disabled = true; + } + + // @TODO: doublecheck this! + let currencySymbol = '$'; + if (products && products[1]) { + currencySymbol = getCurrencySymbol(products[1].monthlyPrice.currency); + } else { + currencySymbol = '$'; + } + + const hasOnlyFree = hasOnlyFreeProduct({site}); + + if (hasOnlyFree) { + const freeBenefits = getFreeProductBenefits({site}); + if (!freeProductDescription && !freeBenefits.length) { + return null; + } + cardClass += ' only-free'; + } + return ( <> - {/* Standard, desktop card */}
{ e.stopPropagation(); setSelectedProduct('free'); }}> -
- { - setSelectedProduct('free'); - }} /> +

{getFreeTierTitle({site})}

- {freeProductDescription ?
{freeProductDescription}
: ''} - + {(!hasOnlyFree ? +
+
+ 1 ? ' long' : '')}>{currencySymbol} + 0 +
+ {/*
*/} +
+ : '')}
-
-
- $ - 0 +
+
+ {freeProductDescription ?
{freeProductDescription}
: ''} +
-
+ {(!hasOnlyFree ? +
+ +
+ : '')}
- - {/* Vertical version */} -
{ - e.stopPropagation(); - setSelectedProduct('free'); - }}> - { - setSelectedProduct('free'); - }} /> -

{getFreeTierTitle({site})}

-
- $ - 0 - /{selectedInterval} -
- {freeProductDescription ?
{freeProductDescription}
: ''} - -
); } +function ProductCard({product, products, selectedInterval, handleChooseSignup}) { + const {selectedProduct, setSelectedProduct} = useContext(ProductsContext); + const {action} = useContext(AppContext); + + const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card'; + let disabled = (action === 'signup:running') ? true : false; + + if (isCookiesDisabled()) { + disabled = true; + } + + return ( + <> +
{ + e.stopPropagation(); + setSelectedProduct(product.id); + }}> +
+

{product.name}

+ +
+
+
+ {product.description ?
{product.description}
: ''} + +
+
+ +
+
+
+ + ); +} + +function ProductCards({products, selectedInterval, handleChooseSignup}) { + return products.map((product) => { + if (product.id === 'free') { + return ( + + ); + } + return ( + + ); + }); +} + function YearlyDiscount({discount}) { - if (discount === 0) { + const {site} = useContext(AppContext); + const {portal_plans: portalPlans} = site; + + if (discount === 0 || !portalPlans.includes('monthly')) { return null; } @@ -720,30 +647,21 @@ function YearlyDiscount({discount}) { function ProductPriceSwitch({products, selectedInterval, setSelectedInterval}) { const {site} = useContext(AppContext); - const {selectedProduct} = useContext(ProductsContext); const {portal_plans: portalPlans} = site; if (!portalPlans.includes('monthly') || !portalPlans.includes('yearly')) { return null; } - let yearlyDiscount = 0; - - if (products && selectedProduct !== 'free') { - const product = products.find(prod => prod.id === selectedProduct); - yearlyDiscount = calculateDiscount(product.monthlyPrice.amount, product.yearlyPrice.amount); - } - return ( -
- Monthly - { - const interval = selectedInterval === 'month' ? 'year' : 'month'; - setSelectedInterval(interval); - }} checked={selectedInterval === 'year'} /> - - Yearly - - +
+
+ + +
); } @@ -780,7 +698,7 @@ function getActiveInterval({portalPlans, selectedInterval = 'year'}) { } } -function ProductsSection({onPlanSelect, products, type = null}) { +function ProductsSection({onPlanSelect, products, type = null, handleChooseSignup}) { const {site} = useContext(AppContext); const {portal_plans: portalPlans} = site; const defaultInterval = getActiveInterval({portalPlans}); @@ -821,14 +739,17 @@ function ProductsSection({onPlanSelect, products, type = null}) { setSelectedProduct }}>
- + + {(!(hasOnlyFreeProduct({site})) ? + + : '')}
- +
@@ -836,7 +757,7 @@ function ProductsSection({onPlanSelect, products, type = null}) { } export function ChangeProductSection({onPlanSelect, selectedPlan, products, type = null}) { - const {site, member, brandColor} = useContext(AppContext); + const {site, member} = useContext(AppContext); const {portal_plans: portalPlans} = site; const activePrice = getMemberActivePrice({member}); const activeMemberProduct = getProductFromPrice({site, priceId: activePrice.id}); @@ -845,7 +766,7 @@ export function ChangeProductSection({onPlanSelect, selectedPlan, products, type const [selectedInterval, setSelectedInterval] = useState(defaultInterval); const [selectedProduct, setSelectedProduct] = useState(defaultProductId); - const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct}); + // const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct}); const activeInterval = getActiveInterval({portalPlans, selectedInterval}); useEffect(() => { @@ -882,9 +803,9 @@ export function ChangeProductSection({onPlanSelect, selectedPlan, products, type />
- +
- onPlanSelect(null, selectedPrice?.id)} isRunning={false} disabled={!selectedPrice?.id || (activePrice.id === selectedPrice?.id)} @@ -892,69 +813,74 @@ export function ChangeProductSection({onPlanSelect, selectedPlan, products, type brandColor={brandColor} label={'Continue'} style={{height: '40px', width: '100%', marginTop: '24px'}} - /> + /> */} ); } -function CurrentPlanLabel({selectedPrice, activePrice}) { - if (selectedPrice.id === activePrice.id) { - return ( -
- Current Plan -
- ); - } - return null; -} - function ProductDescription({product, selectedPrice, activePrice}) { if (product?.description) { return (
{product.description} -
); } return null; } -function ChangeProductCard({product}) { +function ChangeProductCard({product, onPlanSelect}) { const {member} = useContext(AppContext); const {selectedProduct, setSelectedProduct, selectedInterval} = useContext(ProductsContext); - const cardClass = selectedProduct === product.id ? 'gh-portal-product-card vertical checked' : 'gh-portal-product-card vertical'; + const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card'; const monthlyPrice = product.monthlyPrice; const yearlyPrice = product.yearlyPrice; const memberActivePrice = getMemberActivePrice({member}); const selectedPrice = selectedInterval === 'month' ? monthlyPrice : yearlyPrice; + const currentPlan = (selectedPrice.id === memberActivePrice.id); + return ( -
{ +
{ e.stopPropagation(); setSelectedProduct(product.id); }}> - { - setSelectedProduct(product.id); - }} /> -

{product.name}

- - {/* {product.description ?
{product.description}
: ''} */} - - +
+

{product.name}

+ +
+
+
+ {product.description ? : ''} + +
+ {(currentPlan ? +
+ Current plan +
+ : +
+ +
)} +
); } -function ChangeProductCards({products}) { +function ChangeProductCards({products, onPlanSelect}) { return products.map((product) => { if (product.id === 'free') { return null; } return ( - + ); }); } diff --git a/ghost/portal/src/components/common/SiteTitleBackButton.js b/ghost/portal/src/components/common/SiteTitleBackButton.js new file mode 100644 index 0000000000..04a530d006 --- /dev/null +++ b/ghost/portal/src/components/common/SiteTitleBackButton.js @@ -0,0 +1,15 @@ +import React from 'react'; +import AppContext from '../../AppContext'; + +export default class SiteTitleBackButton extends React.Component { + static contextType = AppContext; + + render() { + const {site} = this.context; + return ( + <> + + + ); + } +} \ No newline at end of file diff --git a/ghost/portal/src/components/common/Switch.js b/ghost/portal/src/components/common/Switch.js index bb35a456fa..e57c90f071 100644 --- a/ghost/portal/src/components/common/Switch.js +++ b/ghost/portal/src/components/common/Switch.js @@ -6,8 +6,8 @@ export const SwitchStyles = ` .gh-portal-for-switch .container { position: relative; display: inline-block; - width: 36px !important; - height: 22px !important; + width: 44px !important; + height: 26px !important; cursor: pointer; } @@ -30,11 +30,10 @@ export const SwitchStyles = ` left: 0; right: 0; bottom: 0; - background: var(--grey13); - border: 1px solid var(--grey11); + background: #e9e9e9; transition: .3s; - width: 36px !important; - height: 22px !important; + width: 44px !important; + height: 26px !important; border-radius: 999px; transition: background 0.15s ease-in-out, border-color 0.15s ease-in-out; cursor: pointer; @@ -48,13 +47,12 @@ export const SwitchStyles = ` .gh-portal-for-switch .input-toggle-component:before { position: absolute; content: ""; - top: 2px !important; - left: 2px !important; - height: 16px !important; - width: 16px !important; + top: 3px !important; + left: 3px !important; + height: 20px !important; + width: 20px !important; background-color: white; transition: .3s; - box-shadow: 0 0 1px rgba(0,0,0,.3), 0 4px 6px rgba(0,0,0,.1); border-radius: 999px; } @@ -64,7 +62,7 @@ export const SwitchStyles = ` } .gh-portal-for-switch input:checked + .input-toggle-component:before { - transform: translateX(14px); + transform: translateX(18px); box-shadow: none; } diff --git a/ghost/portal/src/components/pages/AccountHomePage.js b/ghost/portal/src/components/pages/AccountHomePage.js index 480ee844d8..c6b18469d1 100644 --- a/ghost/portal/src/components/pages/AccountHomePage.js +++ b/ghost/portal/src/components/pages/AccountHomePage.js @@ -12,11 +12,6 @@ import {useContext} from 'react'; const React = require('react'); export const AccountHomePageStyles = ` - .gh-portal-account-main { - background: var(--grey13); - padding: 32px 32px 0; - } - .gh-portal-account-header { display: flex; flex-direction: column; @@ -29,14 +24,11 @@ export const AccountHomePageStyles = ` } .gh-portal-account-data { - margin-bottom: 32px; + margin-bottom: 40px; } footer.gh-portal-account-footer { display: flex; - padding: 32px; - height: 104px; - border-top: 1px solid #eaeaea; } .gh-portal-account-footer.paid { @@ -45,6 +37,7 @@ export const AccountHomePageStyles = ` .gh-portal-account-footermenu { display: flex; + align-items: center; list-style: none; padding: 0; margin: 0; diff --git a/ghost/portal/src/components/pages/AccountPlanPage.js b/ghost/portal/src/components/pages/AccountPlanPage.js index 771b4cbc03..eff8245880 100644 --- a/ghost/portal/src/components/pages/AccountPlanPage.js +++ b/ghost/portal/src/components/pages/AccountPlanPage.js @@ -3,18 +3,23 @@ import AppContext from '../../AppContext'; import ActionButton from '../common/ActionButton'; import CloseButton from '../common/CloseButton'; import BackButton from '../common/BackButton'; -import PlansSection, {MultipleProductsPlansSection, SingleProductPlansSection} from '../common/PlansSection'; +import {MultipleProductsPlansSection} from '../common/PlansSection'; import {getDateString} from '../../utils/date-time'; -import {formatNumber, getAvailablePrices, getFilteredPrices, getMemberActivePrice, getMemberSubscription, getPriceFromSubscription, getProductFromPrice, getSubscriptionFromId, getUpgradeProducts, hasMultipleProducts, hasMultipleProductsFeature, isPaidMember} from '../../utils/helpers'; +import {formatNumber, getAvailablePrices, getFilteredPrices, getMemberActivePrice, getMemberSubscription, getPriceFromSubscription, getProductFromPrice, getSubscriptionFromId, getUpgradeProducts, hasMultipleProductsFeature, isPaidMember} from '../../utils/helpers'; export const AccountPlanPageStyles = ` + .account-plan.full-size .gh-portal-main-title { + font-size: 3.2rem; + margin-top: 32px; + } + .gh-portal-accountplans-main { margin-top: 24px; margin-bottom: 0; } .gh-portal-expire-container { - margin: 24px 0 0; + margin: 32px 0 0; } .gh-portal-cancellation-form p { @@ -46,14 +51,13 @@ function getConfirmationPageTitle({confirmationType}) { } const Header = ({onBack, showConfirmation, confirmationType}) => { - const {member, brandColor, lastPage} = useContext(AppContext); + const {member} = useContext(AppContext); let title = isPaidMember({member}) ? 'Change plan' : 'Choose a plan'; if (showConfirmation) { title = getConfirmationPageTitle({confirmationType}); } return (
- onBack(e)} hidden={!lastPage && !showConfirmation} />

{title}

); @@ -122,7 +126,7 @@ const PlanConfirmationSection = ({plan, type, onConfirm}) => { if (type === 'changePlan') { return ( <> -
+

Account

@@ -205,46 +209,16 @@ const ChangePlanSection = ({plans, selectedPlan, onPlanSelect, onCancelSubscript ); }; -function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, changePlan = false}) { +function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, onPlanCheckout, changePlan = false}) { const {site, member} = useContext(AppContext); const products = getUpgradeProducts({site, member}); - if (hasMultipleProductsFeature({site})) { - if (changePlan === true) { - return ( - - ); - } else if (hasMultipleProducts({site})) { - return ( - - ); - } else { - return ( - - ); - } - } return ( - ); } @@ -253,8 +227,8 @@ function PlansOrProductSection({showLabel, plans, selectedPlan, onPlanSelect, ch const UpgradePlanSection = ({ plans, selectedPlan, onPlanSelect, onPlanCheckout }) => { - const {action, brandColor} = useContext(AppContext); - const isRunning = ['checkoutPlan:running'].includes(action); + // const {action, brandColor} = useContext(AppContext); + // const isRunning = ['checkoutPlan:running'].includes(action); let singlePlanClass = ''; if (plans.length === 1) { singlePlanClass = 'singleplan'; @@ -267,16 +241,17 @@ const UpgradePlanSection = ({ plans={plans} selectedPlan={selectedPlan} onPlanSelect={onPlanSelect} + onPlanCheckout={onPlanCheckout} />
- onPlanCheckout(e)} isRunning={isRunning} isPrimary={true} brandColor={brandColor} label={'Continue'} style={{height: '40px', width: '100%', marginTop: '24px'}} - /> + /> */}
); }; @@ -381,7 +356,10 @@ export default class AccountPlanPage extends React.Component { onPlanCheckout(e, priceId) { const {onAction, member} = this.context; - const {confirmationPlan, selectedPlan} = this.state; + let {confirmationPlan, selectedPlan} = this.state; + if (priceId) { + selectedPlan = priceId; + } if (isPaidMember({member})) { const subscription = getMemberSubscription({member}); const subscriptionId = subscription ? subscription.id : ''; @@ -459,16 +437,18 @@ export default class AccountPlanPage extends React.Component { if (confirmationType === 'cancel') { return this.onCancelSubscriptionConfirmation(data); } else if (['changePlan', 'subscribe'].includes(confirmationType)) { - return this.onPlanCheckout(data); + return this.onPlanCheckout(); } } render() { const plans = this.prices; const {selectedPlan, showConfirmation, confirmationPlan, confirmationType} = this.state; + const {lastPage} = this.context; return ( <>
+ this.onBack(e)} hidden={!lastPage && !showConfirmation} />
this.onBack(e)} diff --git a/ghost/portal/src/components/pages/AccountPlanPage.test.js b/ghost/portal/src/components/pages/AccountPlanPage.test.js index 0bda4d92c9..d8f178d9e8 100644 --- a/ghost/portal/src/components/pages/AccountPlanPage.test.js +++ b/ghost/portal/src/components/pages/AccountPlanPage.test.js @@ -12,13 +12,15 @@ const setup = (overrides) => { } } ); - const monthlyCheckboxEl = utils.getByLabelText('Monthly'); - const yearlyCheckboxEl = utils.getByLabelText('Yearly'); + const monthlyCheckboxEl = utils.queryByRole('button', {name: 'Monthly'}); + const yearlyCheckboxEl = utils.queryByRole('button', {name: 'Yearly'}); const continueBtn = utils.queryByRole('button', {name: 'Continue'}); + const chooseBtns = utils.queryAllByRole('button', {name: 'Choose'}); return { monthlyCheckboxEl, yearlyCheckboxEl, continueBtn, + chooseBtns, mockOnActionFn, context, ...utils @@ -44,26 +46,24 @@ const customSetup = (overrides) => { describe('Account Plan Page', () => { test('renders', () => { - const {monthlyCheckboxEl, yearlyCheckboxEl, continueBtn} = setup(); + const {monthlyCheckboxEl, yearlyCheckboxEl, chooseBtns} = setup(); expect(monthlyCheckboxEl).toBeInTheDocument(); expect(yearlyCheckboxEl).toBeInTheDocument(); - expect(continueBtn).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(1); }); test('can choose plan and continue', async () => { const siteData = getSiteData({ products: getProductsData({numOfProducts: 1}) }); - const {mockOnActionFn, monthlyCheckboxEl, yearlyCheckboxEl, continueBtn} = setup({site: siteData}); + const {mockOnActionFn, monthlyCheckboxEl, yearlyCheckboxEl, chooseBtns} = setup({site: siteData}); fireEvent.click(monthlyCheckboxEl); - expect(monthlyCheckboxEl.checked).toEqual(false); + expect(monthlyCheckboxEl.className).toEqual('gh-portal-btn active'); fireEvent.click(yearlyCheckboxEl); - expect(yearlyCheckboxEl.checked).toEqual(true); - expect(continueBtn).toBeEnabled(); - - fireEvent.click(continueBtn); - expect(mockOnActionFn).toHaveBeenCalledWith('checkoutPlan', {plan: siteData.products[0].monthlyPrice.id}); + expect(yearlyCheckboxEl.className).toEqual('gh-portal-btn active'); + fireEvent.click(chooseBtns[0]); + expect(mockOnActionFn).toHaveBeenCalledWith('checkoutPlan', {plan: siteData.products[0].yearlyPrice.id}); }); test('can cancel subscription for member on hidden tier', async () => { diff --git a/ghost/portal/src/components/pages/OfferPage.js b/ghost/portal/src/components/pages/OfferPage.js index d052b424f2..24ba8e1a5c 100644 --- a/ghost/portal/src/components/pages/OfferPage.js +++ b/ghost/portal/src/components/pages/OfferPage.js @@ -3,216 +3,126 @@ import AppContext from '../../AppContext'; import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg'; import CloseButton from '../common/CloseButton'; import InputForm from '../common/InputForm'; -import {getCurrencySymbol, getProductFromId, hasMultipleProductsFeature, isSameCurrency} from '../../utils/helpers'; +import {getCurrencySymbol, getProductFromId, hasMultipleProductsFeature, isSameCurrency, formatNumber} from '../../utils/helpers'; import {ValidateInputForm} from '../../utils/form'; const React = require('react'); -export const OfferPageStyles = ` - .gh-portal-offer { - padding-bottom: 0; - overflow: unset; - max-height: unset; - } +export const OfferPageStyles = ({site}) => { + return ` +.gh-portal-offer { + padding-bottom: 0; + overflow: unset; + max-height: unset; +} - .gh-portal-offer h4 { - color: var(--grey0); - margin: 0 0 7px; - } +.gh-portal-offer-container { + display: flex; + flex-direction: column; +} - .gh-portal-offer p { - color: var(--grey3); - font-size: 1.25rem; - font-weight: 400; - margin: 0 0 6px; - } +.gh-portal-plans-container.offer { + justify-content: space-between; + border-color: var(--grey12); + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + padding: 12px 16px; + font-size: 1.3rem; +} - .gh-portal-offer-container { - display: flex; - flex-direction: column; - } +.gh-portal-offer-bar { + position: relative; + padding: 26px 28px 28px; + margin-bottom: 24px; + /*border: 1px dashed var(--brandcolor);*/ + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='99.9%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='%23C3C3C3' stroke-width='3' stroke-dasharray='3%2c 9' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + border-radius: 6px; +} - .gh-portal-plans-container.offer { - justify-content: space-between; - border-color: var(--grey12); - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; - padding: 12px 16px; - font-size: 1.3rem; - } +.gh-portal-offer-title { + display: flex; + justify-content: space-between; + align-items: center; +} - .gh-portal-offer-bar { - position: relative; - padding: 20px; - margin-bottom: 24px; - } +.gh-portal-offer-title h4 { + font-size: 1.8rem; + margin: 0 80px 0 0; + width: 100%; +} - .gh-portal-offer-bar::before { - border-radius: 5px; - position: absolute; - display: block; - content: ""; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: var(--brandcolor); - opacity: 0.1; - z-index: -1; - } +.gh-portal-offer-title h4.placeholder { + opacity: 0.4; +} - .gh-portal-offer-title { - display: flex; - justify-content: space-between; - margin-right: -20px; - } +.gh-portal-offer-bar .gh-portal-discount-label { + position: absolute; + top: 23px; + right: 25px; +} - .gh-portal-offer-title h4 { - font-weight: 500; - font-size: 1.7rem; - margin-bottom: 0; - line-height: 1.3em; - } +.gh-portal-offer-bar p { + padding-bottom: 0; + margin: 12px 0 0; +} - .gh-portal-offer-tag { - background: var(--brandcolor); - color: #fff; - padding: 4px 8px 4px 12px; - font-weight: 500; - font-size: 1.2rem; - text-transform: uppercase; - letter-spacing: 0.5px; - border-radius: 999px 0 0 999px; - max-height: 22px; - white-space: nowrap; - } +.gh-portal-offer-title h4 + p { + margin: 12px 0 0; +} - .gh-portal-offer-bar p { - padding-bottom: 0; - font-size: 1.35rem; - } +.gh-portal-offer-details .gh-portal-plan-name, +.gh-portal-offer-details p { + margin-right: 8px; +} - .gh-portal-offer-title h4 + p { - margin: 12px 0 0; - } +.gh-portal-offer .footnote { + font-size: 1.35rem; + color: var(--grey8); + margin: 4px 0 0; +} - .gh-portal-offer-details .gh-portal-plan-name, - .gh-portal-offer-details p { - margin-right: 8px; - } +.offer .gh-portal-product-card { + max-width: unset; + min-height: 0; +} - .gh-portal-offer .gh-portal-plan-section { - cursor: auto; - padding: 20px; - flex-direction: row; - justify-content: space-between; - } +.offer .gh-portal-product-card .gh-portal-product-card-pricecontainer { + margin-top: 0px; +} - .gh-portal-offer .gh-portal-plan-section:before { - display: none; - } +.offer .gh-portal-product-card-header { + display: flex; + flex-direction: column; + align-items: flex-start; +} - .gh-portal-offer-container.bordered { - border: 1px solid var(--grey11) !important; - border-radius: 5px; - margin-bottom: 5px; - } +.gh-portal-offer-oldprice { + display: flex; + position: relative; + font-size: 1.8rem; + font-weight: 300; + color: var(--grey8); + line-height: 1; + white-space: nowrap; + margin: 16px 0 4px; +} - .gh-portal-offer-container.bordered p.footnote { - margin: 0; - } +.gh-portal-offer-oldprice:after { + position: absolute; + display: block; + content: ""; + left: 0; + top: 50%; + right: 0; + height: 1px; + background: var(--grey8); +} - .gh-portal-offer-container.bordered .gh-portal-plan-section { - padding: 12px 20px; - } - - .gh-portal-offer-planname { - padding-right: 20px; - } - - .gh-portal-offer .gh-portal-plan-name { - margin: 0; - text-align: left; - line-height: 1.5em; - } - - .gh-portal-offer .footnote { - color: var(--grey7); - margin: 0 0 12px; - } - - .gh-portal-offer-price { - display: flex; - flex-direction: column; - align-items: flex-end; - margin: 0; - } - - .gh-portal-offer-price .old { - text-decoration: line-through; - color: var(--grey5); - line-height: 1; - white-space: nowrap; - } - - .gh-portal-offer-price .new { - display: flex; - align-items: flex-start; - margin-top: 6px; - white-space: nowrap; - } - - .gh-portal-offer-price .new .currency { - font-weight: 500; - line-height: 1; - font-size: 1.5rem; - margin-right: 1px; - white-space: nowrap; - } - - .gh-portal-offer-price .new .value { - font-size: 2.4rem; - font-weight: 500; - white-space: nowrap; - } - - .gh-portal-offer-details p { - margin-bottom: 12px; - } - - .gh-portal-offer .gh-portal-product-benefit { - margin-bottom: 4px; - } - - .gh-portal-offer .gh-portal-singleproduct-benefits { - padding: 16px 20px 12px !important - } - - .gh-portal-offer .gh-portal-singleproduct-benefits:not(.no-benefits) .gh-portal-product-description { - text-align: left; - padding-left: 0; - } - - .gh-portal-offer .gh-portal-singleproduct-benefits .gh-portal-product-benefit { - padding: 0; - } - - .gh-portal-offer .gh-portal-singleproduct-benefits:not(.no-benefits) .gh-portal-product-description { - border: none; - padding-bottom: 0; - } - - .gh-portal-offer .gh-portal-product-benefits { - padding-bottom: 0; - } - - .gh-portal-offer-planname .gh-portal-offer-tag { - display: inline-block; - border-radius: 0 999px 999px 0; - margin: -8px 0 0 -20px; - padding-left: 20px; - } -`; +.gh-portal-offer-details p { + margin-bottom: 12px; +} + `; +}; export default class OfferPage extends React.Component { static contextType = AppContext; @@ -386,6 +296,7 @@ export default class OfferPage extends React.Component { label={label} isRunning={isRunning} tabIndex='3' + classes={'sticky bottom'} /> ); } @@ -412,13 +323,20 @@ export default class OfferPage extends React.Component { renderOfferTag() { const {pageData: offer} = this.context; + + if (offer.amount <= 0) { + return ( + <> + ); + } + if (offer.type === 'fixed') { return ( -
{getCurrencySymbol(offer.currency)}{offer.amount / 100} off
+
{getCurrencySymbol(offer.currency)}{offer.amount / 100} off
); } return ( -
{offer.amount}% off
+
{offer.amount}% off
); } @@ -431,7 +349,7 @@ export default class OfferPage extends React.Component { return (
- {benefit.name} +
{benefit.name}
); }); @@ -525,75 +443,55 @@ export default class OfferPage extends React.Component { const price = offer.cadence === 'month' ? product.monthlyPrice : product.yearlyPrice; const updatedPrice = this.getUpdatedPrice({offer, product}); const benefits = product.benefits || []; - let planNameContainerClass = 'gh-portal-plans-container gh-portal-offer-container has-multiple-products'; - planNameContainerClass += !benefits.length && !product.description ? ' bordered' : ''; + + const currencyClass = (getCurrencySymbol(price.currency)).length > 1 ? 'long' : ''; + return ( <>
{this.renderFormHeader()} - {(offer.display_title || offer.display_description ? -
-
-
- {(offer.display_title ? -

{offer.display_title}

- : '')} - - {(offer.display_description ?

{offer.display_description}

: '')} -
- - {this.renderOfferTag()} -
+
+
+ {(offer.display_title ?

{offer.display_title}

:

Black Friday

)} + {this.renderOfferTag()}
- : '')} + {(offer.display_description ?

{offer.display_description}

: '')} +
{this.renderForm()} -
-
-
- {(!offer.display_title && !offer.display_description ? - this.renderOfferTag() - : '')} -

{product.name} - {(offer.cadence === 'month' ? 'Monthly' : 'Yearly')}

- {(!benefits.length && !product.description ? - this.renderOfferMessage({offer, product}) - : '')} -
-
-
-
-
{getCurrencySymbol(price.currency)}{price.amount / 100}
-
- {getCurrencySymbol(price.currency)} - {this.renderRoundedPrice(updatedPrice)} -
-
+
+
+

{product.name} - {(offer.cadence === 'month' ? 'Monthly' : 'Yearly')}

+
{getCurrencySymbol(price.currency)} {formatNumber(price.amount / 100)}
+
+
+ {getCurrencySymbol(price.currency)} + {formatNumber(this.renderRoundedPrice(updatedPrice))} + /year
+ {this.renderOfferMessage({offer, product})}
- {(benefits.length || product.description ? -
- {(product.description ? -
{product.description}
- : '')} - - {(benefits.length ? - this.renderBenefits({product}) - : '')} - - {this.renderOfferMessage({offer, product})} +
+
+
+ {(product.description ?
{product.description}
: '')} + {(benefits.length ? this.renderBenefits({product}) : '')} +
- : '')} + +
+ {this.renderSubmitButton()} +
+ + {this.renderLoginMessage()} +
-
- {this.renderSubmitButton()} - {this.renderLoginMessage()} -
); } diff --git a/ghost/portal/src/components/pages/SigninPage.js b/ghost/portal/src/components/pages/SigninPage.js index 90a4cb4db9..8c52f9bd9a 100644 --- a/ghost/portal/src/components/pages/SigninPage.js +++ b/ghost/portal/src/components/pages/SigninPage.js @@ -1,5 +1,6 @@ import ActionButton from '../common/ActionButton'; import CloseButton from '../common/CloseButton'; +// import SiteTitleBackButton from '../common/SiteTitleBackButton'; import AppContext from '../../AppContext'; import InputForm from '../common/InputForm'; import {ValidateInputForm} from '../../utils/form'; @@ -133,12 +134,12 @@ export default class SigninPage extends React.Component { } renderFormHeader() { - const siteTitle = this.context.site.title || 'Site Title'; + // const siteTitle = this.context.site.title || 'Site Title'; return (
{this.renderSiteLogo()} -

Log in to {siteTitle}

+

Sign in

); } @@ -146,15 +147,20 @@ export default class SigninPage extends React.Component { render() { return ( <> -
- - {this.renderFormHeader()} - {this.renderForm()} + {/*
+ +
*/} + +
+
+ {this.renderFormHeader()} + {this.renderForm()} +
+
+ {this.renderSubmitButton()} + {this.renderSignupMessage()} +
-
- {this.renderSubmitButton()} - {this.renderSignupMessage()} -
); } diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index c6f0f5b1ff..1af4e7a1cc 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -1,234 +1,209 @@ import ActionButton from '../common/ActionButton'; -import CloseButton from '../common/CloseButton'; import AppContext from '../../AppContext'; -import PlansSection, {SingleProductPlansSection} from '../common/PlansSection'; +import CloseButton from '../common/CloseButton'; +import SiteTitleBackButton from '../common/SiteTitleBackButton'; import ProductsSection from '../common/ProductsSection'; import InputForm from '../common/InputForm'; import {ValidateInputForm} from '../../utils/form'; -import {getSiteProducts, getSitePrices, hasMultipleProducts, hasOnlyFreePlan, isInviteOnlySite, getAvailableProducts, hasMultipleProductsFeature, freeHasBenefitsOrDescription} from '../../utils/helpers'; +import {getSiteProducts, getSitePrices, hasOnlyFreePlan, isInviteOnlySite, freeHasBenefitsOrDescription, hasOnlyFreeProduct, getFreeProductBenefits, getFreeTierDescription} from '../../utils/helpers'; import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg'; -import {ReactComponent as GhostLogo} from '../../images/ghost-logo-small.svg'; const React = require('react'); export const SignupPageStyles = ` +.gh-portal-back-sitetitle { + position: absolute; + top: 35px; + left: 32px; +} + +.gh-portal-back-sitetitle .gh-portal-btn { + padding: 0; + border: 0; + font-size: 1.5rem; + height: auto; + line-height: 1em; + color: var(--grey1); +} + +.gh-portal-popup-wrapper:not(.full-size) .gh-portal-back-sitetitle, +.gh-portal-popup-wrapper.preview .gh-portal-back-sitetitle { + display: none; +} + +.gh-portal-signup-logo { + position: relative; + display: block; + background-position: 50%; + background-size: cover; + border-radius: 2px; + width: 60px; + height: 60px; + margin: 12px 0 10px; +} + +.gh-portal-signup-header, +.gh-portal-signin-header { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 32px; + margin-bottom: 32px; +} + +.gh-portal-popup-wrapper.full-size .gh-portal-signup-header { + margin-top: 32px; +} + +.gh-portal-signup-header .gh-portal-main-title, +.gh-portal-signin-header .gh-portal-main-title { + margin-top: 12px; +} + +.gh-portal-signup-logo + .gh-portal-main-title { + margin: 4px 0 0; +} + +.gh-portal-signup-header .gh-portal-main-subtitle { + font-size: 1.5rem; + text-align: center; + line-height: 1.45em; + margin: 4px 0 0; + color: var(--grey3); +} + +.gh-portal-logged-out-form-container { + width: 100%; + max-width: 420px; + margin: 0 auto; +} + +.signup .gh-portal-input-section:last-of-type { + margin-bottom: 40px; +} + +.gh-portal-signup-message { + display: flex; + justify-content: center; + color: var(--grey4); + font-size: 1.5rem; + margin-top: 8px; +} + +.full-size .gh-portal-signup-message { + margin-bottom: 40px; +} + +@media (max-width: 480px) { + .preview .gh-portal-products + .gh-portal-signup-message { + margin-bottom: 40px; + } +} + +.gh-portal-signup-message button { + font-size: 1.4rem; + font-weight: 600; + margin-left: 4px !important; +} + +.gh-portal-signup-message button span { + display: inline-block; + padding-bottom: 2px; + margin-bottom: -2px; +} + +.gh-portal-content.signup.invite-only { + background: none; +} + +footer.gh-portal-signup-footer, +footer.gh-portal-signin-footer { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + padding-top: 24px; + height: unset; +} + +.gh-portal-content.signup, +.gh-portal-content.signin { + max-height: unset !important; + padding-bottom: 0; +} + +.gh-portal-content.signin { + padding-bottom: 4px; +} + +.gh-portal-content.signup .gh-portal-section { + margin-bottom: 0; +} + +.gh-portal-content.signup.noplan { + margin-bottom: -8px; +} + +.gh-portal-content.signup.single-field { + margin-bottom: 4px; +} + +.gh-portal-content.signup.single-field .gh-portal-input, +.gh-portal-content.signin .gh-portal-input { + margin-bottom: 8px; +} + +.gh-portal-content.signup.single-field + .gh-portal-signup-footer, +footer.gh-portal-signin-footer { + padding-top: 12px; +} + +.gh-portal-content.signin .gh-portal-section { + margin-bottom: 0; +} + +footer.gh-portal-signup-footer.invite-only { + height: unset; +} + +footer.gh-portal-signup-footer.invite-only .gh-portal-signup-message { + margin-top: 0; +} + +.gh-portal-invite-only-notification { + margin: 8px 32px 24px; + padding: 0; + text-align: center; + color: var(--grey2); +} + +.gh-portal-icon-invitation { + width: 44px; + height: 44px; + margin: 12px 0 2px; +} + +.gh-portal-popup-wrapper.full-size .gh-portal-popup-container.preview footer.gh-portal-signup-footer { + padding-bottom: 32px; +} + +@media (min-width: 480px) { + +} + +@media (max-width: 480px) { .gh-portal-signup-logo { - position: relative; - display: block; - background-position: 50%; - background-size: cover; - border-radius: 2px; - width: 56px; - height: 56px; - margin: 12px 0 10px; + width: 48px; + height: 48px; } +} - .gh-portal-signup-header, - .gh-portal-signin-header { - display: flex; - flex-direction: column; - align-items: center; - padding: 0 32px 24px; - } - - .gh-portal-signup-header .gh-portal-main-title, - .gh-portal-signin-header .gh-portal-main-title { - margin-top: 12px; - } - - .gh-portal-signup-logo + .gh-portal-main-title { - margin: 4px 0 0; - } - - .gh-portal-signup-header .gh-portal-main-subtitle { - font-size: 1.5rem; - text-align: center; - line-height: 1.45em; - margin: 4px 0 0; - color: var(--grey3); - } - - .gh-portal-signup-header.nodivider { - border: none; - margin-bottom: 0; - } - - .gh-portal-signup-message { - display: flex; - justify-content: center; - color: var(--grey4); - font-size: 1.3rem; - letter-spacing: 0.2px; - margin-top: 8px; - } - - .gh-portal-signup-message button { - font-size: 1.3rem; - font-weight: 600; - margin-left: 4px; - } - - .gh-portal-signup-message button span { - display: inline-block; - padding-bottom: 2px; - margin-bottom: -2px; - } - - .gh-portal-content.signup.invite-only { - background: none; - } - - footer.gh-portal-signup-footer, - footer.gh-portal-signin-footer { - display: flex; - flex-direction: column; - align-items: center; - position: relative; - padding-top: 24px; - height: unset; - } - - .gh-portal-content.signup, - .gh-portal-content.signin { - max-height: unset !important; - padding-bottom: 0; - } - - .gh-portal-content.signin { - padding-bottom: 4px; - } - - .gh-portal-content.signup .gh-portal-section { - margin-bottom: 0; - } - - .gh-portal-content.signup.noplan { - margin-bottom: -8px; - } - - .gh-portal-content.signup.single-field { - margin-bottom: 4px; - } - - .gh-portal-content.signup.single-field .gh-portal-input, - .gh-portal-content.signin .gh-portal-input { - margin-bottom: 8px; - } - - .gh-portal-content.signup.single-field + .gh-portal-signup-footer, - footer.gh-portal-signin-footer { - padding-top: 12px; - } - - .gh-portal-content.signin .gh-portal-section { - margin-bottom: 0; - } - - .gh-portal-content.signup.single-field + footer.gh-portal-signup-footer, - .gh-portal-content.signin + footer.gh-portal-signin-footer { - height: 120px; - } - - footer.gh-portal-signup-footer.invite-only { - height: unset; - } - - footer.gh-portal-signup-footer.invite-only .gh-portal-signup-message { - margin-top: 0; - } - - .gh-portal-popup-wrapper.multiple-products .gh-portal-powered { - display: flex; - margin-top: 48px; - margin-bottom: 0; - padding-bottom: 0 !important; - } - - .gh-portal-invite-only-notification { - margin: 8px 32px; - padding: 0; - text-align: center; - color: var(--grey2); - } - - .gh-portal-icon-invitation { - width: 44px; - height: 44px; - margin: 12px 0 2px; - } - - - /* Multiple products signup */ - - .gh-portal-popup-wrapper.signup.multiple-products .gh-portal-content, - .gh-portal-popup-wrapper.signin.multiple-products .gh-portal-content { - width: 100%; - background: #fff; - } - - .gh-portal-popup-wrapper.multiple-products footer.gh-portal-signup-footer, - .gh-portal-popup-wrapper.multiple-products footer.gh-portal-signin-footer { - flex: 1; - width: 100%; - height: unset; - padding: 0 32px !important; - margin: 24px 32px; - } - - .gh-portal-popup-wrapper.multiple-products footer .gh-portal-btn { - max-width: 420px; - } - - .gh-portal-popup-wrapper.multiple-products footer.gh-portal-signin-footer { - padding-top: 24px; - } - - .gh-portal-powered.multiple-products.signup { - display: none; - } - - @media (max-width: 480px) { - .gh-portal-popup-wrapper.multiple-products .gh-portal-powered { - margin-top: 0; - margin-bottom: -32px; - } - - .gh-portal-popup-wrapper.multiple-products footer.gh-portal-signup-footer, - .gh-portal-popup-wrapper.multiple-products footer.gh-portal-signin-footer { - max-width: unset; - padding: 0 32px !important; - } - - .gh-portal-popup-wrapper.multiple-products.preview footer.gh-portal-signup-footer, - .gh-portal-popup-wrapper.multiple-products.preview footer.gh-portal-signin-footer { - padding-bottom: 32px !important; - } - - .gh-portal-popup-wrapper.signup.multiple-products.preview .gh-portal-content, - .gh-portal-popup-wrapper.signin.multiple-products.preview .gh-portal-content { - overflow: unset; - } - } - - @media (max-width: 390px) { - .gh-portal-popup-wrapper.multiple-products footer.gh-portal-signup-footer, - .gh-portal-popup-wrapper.multiple-products footer.gh-portal-signin-footer { - padding: 0 24px !important; - } - } - - @media (min-width: 480px) and (max-width: 820px) { - .gh-portal-powered.outside { - left: 50%; - transform: translateX(-50%); - } - } - - @media (min-width: 480px) { - .gh-portal-popup-wrapper:not(.multiple-products) .gh-portal-powered { - display: none; - } +@media (min-width: 480px) and (max-width: 820px) { + .gh-portal-powered.outside { + left: 50%; + transform: translateX(-50%); } +} `; class SignupPage extends React.Component { @@ -294,6 +269,25 @@ class SignupPage extends React.Component { }); } + handleChooseSignup(e, plan) { + e.preventDefault(); + this.setState((state) => { + return { + errors: ValidateInputForm({fields: this.getInputFields({state})}) + }; + }, () => { + const {onAction} = this.context; + const {name, email, errors} = this.state; + const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0); + if (!hasFormErrors) { + onAction('signup', {name, email, plan}); + this.setState({ + errors: {} + }); + } + }); + } + handleInputChange(e, field) { const fieldName = field.name; const value = e.target.value; @@ -385,6 +379,8 @@ class SignupPage extends React.Component { let label = 'Continue'; if (hasOnlyFreePlan({site})) { label = 'Sign up'; + } else { + return null; } let isRunning = false; @@ -413,40 +409,13 @@ class SignupPage extends React.Component { ); } - renderPlans() { - const {site, pageQuery} = this.context; - const prices = getSitePrices({site, pageQuery}); - if (hasMultipleProductsFeature({site})) { - const availableProducts = getAvailableProducts({site}); - const product = availableProducts?.[0]; - return ( - { - this.handleSelectPlan(e, id); - }} - /> - ); - } - return ( - { - this.handleSelectPlan(e, id); - }} - /> - ); - } - renderProducts() { const {site, pageQuery} = this.context; const products = getSiteProducts({site, pageQuery}); return ( <> this.handleChooseSignup(...args)} products={products} onPlanSelect={this.handleSelectPlan} /> @@ -470,15 +439,6 @@ class SignupPage extends React.Component { ); } - renderProductsOrPlans() { - const {site} = this.context; - if (hasMultipleProducts({site})) { - return this.renderProducts(); - } else { - return this.renderPlans(); - } - } - renderForm() { const fields = this.getInputFields({state: this.state}); const {site, pageQuery} = this.context; @@ -488,20 +448,40 @@ class SignupPage extends React.Component {

This site is invite-only, contact the owner for access.

+ {this.renderLoginMessage()}
); } + const freeBenefits = getFreeProductBenefits({site}); + const freeDescription = getFreeTierDescription({site}); + const hasOnlyFree = hasOnlyFreeProduct({site}); + const sticky = freeBenefits.length || freeDescription; + return (
- this.handleInputChange(e, field)} - onKeyDown={e => this.onKeyDown(e)} - /> - {this.renderProductsOrPlans()} +
+ this.handleInputChange(e, field)} + onKeyDown={e => this.onKeyDown(e)} + /> +
+
+ {this.renderProducts()} + + {(hasOnlyFree ? +
+
+ {this.renderSubmitButton()} + {this.renderLoginMessage()} +
+
+ : + this.renderLoginMessage())} +
); @@ -533,7 +513,7 @@ class SignupPage extends React.Component { return (
{this.renderSiteLogo()} -

{siteTitle}

+

{siteTitle}

); } @@ -563,64 +543,23 @@ class SignupPage extends React.Component { return {sectionClass, footerClass}; } - renderMultipleProducts() { - let {sectionClass, footerClass} = this.getClassNames(); - - return ( - <> -
- - {this.renderFormHeader()} - {this.renderForm()} -
-
- {this.renderSubmitButton()} - {this.renderLoginMessage()} -
- - ); - } - - renderSingleProduct() { - let {sectionClass, footerClass} = this.getClassNames(); - - return ( - <> -
- - {this.renderFormHeader()} - {this.renderForm()} -
-
- {this.renderSubmitButton()} - {this.renderLoginMessage()} -
- - ); - } - render() { - let {sectionClass, footerClass} = this.getClassNames(); + let {sectionClass} = this.getClassNames(); return ( <> +
+ +
+
- {this.renderFormHeader()} {this.renderForm()}
- */} ); } diff --git a/ghost/portal/src/components/pages/SignupPage.test.js b/ghost/portal/src/components/pages/SignupPage.test.js index 4857c1e483..22f1b2b5f0 100644 --- a/ghost/portal/src/components/pages/SignupPage.test.js +++ b/ghost/portal/src/components/pages/SignupPage.test.js @@ -14,11 +14,13 @@ const setup = (overrides) => { const emailInput = utils.getByLabelText(/email/i); const nameInput = utils.getByLabelText(/name/i); const submitButton = utils.queryByRole('button', {name: 'Continue'}); + const chooseButton = utils.queryAllByRole('button', {name: 'Choose'}); const signinButton = utils.queryByRole('button', {name: 'Sign in'}); return { nameInput, emailInput, submitButton, + chooseButton, signinButton, mockOnActionFn, ...utils @@ -27,16 +29,16 @@ const setup = (overrides) => { describe('SignupPage', () => { test('renders', () => { - const {nameInput, emailInput, submitButton, signinButton} = setup(); + const {nameInput, emailInput, chooseButton, signinButton} = setup(); expect(nameInput).toBeInTheDocument(); expect(emailInput).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseButton).toHaveLength(2); expect(signinButton).toBeInTheDocument(); }); test('can call signup action with name, email and plan', () => { - const {nameInput, emailInput, submitButton, mockOnActionFn} = setup(); + const {nameInput, emailInput, chooseButton, mockOnActionFn} = setup(); const nameVal = 'J Smith'; const emailVal = 'jsmith@example.com'; const planVal = 'free'; @@ -46,7 +48,7 @@ describe('SignupPage', () => { expect(nameInput).toHaveValue(nameVal); expect(emailInput).toHaveValue(emailVal); - fireEvent.click(submitButton); + fireEvent.click(chooseButton[0]); expect(mockOnActionFn).toHaveBeenCalledWith('signup', {email: emailVal, name: nameVal, plan: planVal}); }); diff --git a/ghost/portal/src/images/icons/checkmark.svg b/ghost/portal/src/images/icons/checkmark.svg index 3c3b725e50..e4e3539725 100644 --- a/ghost/portal/src/images/icons/checkmark.svg +++ b/ghost/portal/src/images/icons/checkmark.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/ghost/portal/src/images/icons/close.svg b/ghost/portal/src/images/icons/close.svg index 2b09d5acb2..76084490e8 100644 --- a/ghost/portal/src/images/icons/close.svg +++ b/ghost/portal/src/images/icons/close.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ghost/portal/src/tests/SignupFlow.test.js b/ghost/portal/src/tests/SignupFlow.test.js index 91d7e8ae38..8317b2378c 100644 --- a/ghost/portal/src/tests/SignupFlow.test.js +++ b/ghost/portal/src/tests/SignupFlow.test.js @@ -37,9 +37,10 @@ const offerSetup = async ({site, member = null, offer}) => { const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); const siteTitle = within(popupIframeDocument).queryByText(site.title); - const offerName = within(popupIframeDocument).queryByText(offer.name); + const offerName = within(popupIframeDocument).queryByText(offer.display_title); const offerDescription = within(popupIframeDocument).queryByText(offer.display_description); const freePlanTitle = within(popupIframeDocument).queryByText('Free'); @@ -56,6 +57,7 @@ const offerSetup = async ({site, member = null, offer}) => { nameInput, signinButton, submitButton, + chooseBtns, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, @@ -93,6 +95,7 @@ const setup = async ({site, member = null}) => { const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); const siteTitle = within(popupIframeDocument).queryByText(site.title); const freePlanTitle = within(popupIframeDocument).queryByText('Free'); @@ -109,6 +112,7 @@ const setup = async ({site, member = null}) => { nameInput, signinButton, submitButton, + chooseBtns, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, @@ -144,6 +148,7 @@ const multiTierSetup = async ({site, member = null}) => { const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); const siteTitle = within(popupIframeDocument).queryByText(site.title); const freePlanTitle = within(popupIframeDocument).queryAllByText(/free$/i); @@ -166,6 +171,7 @@ const multiTierSetup = async ({site, member = null}) => { yearlyPlanTitle, fullAccessTitle, freePlanDescription, + chooseBtns, ...utils }; }; @@ -174,8 +180,8 @@ describe('Signup', () => { describe('as free member on single tier site', () => { test('with default settings', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, - siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, chooseBtns } = await setup({ site: FixtureSite.singleTier.basic }); @@ -188,16 +194,17 @@ describe('Signup', () => { expect(freePlanTitle).toBeInTheDocument(); expect(monthlyPlanTitle).toBeInTheDocument(); expect(yearlyPlanTitle).toBeInTheDocument(); - expect(fullAccessTitle).toBeInTheDocument(); + // expect(fullAccessTitle).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + // expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(2); fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); expect(emailInput).toHaveValue('jamie@example.com'); expect(nameInput).toHaveValue('Jamie Larsen'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[0]); expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ email: 'jamie@example.com', name: 'Jamie Larsen', @@ -209,8 +216,8 @@ describe('Signup', () => { test('without name field', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, - siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, chooseBtns } = await setup({ site: FixtureSite.singleTier.withoutName }); @@ -223,14 +230,14 @@ describe('Signup', () => { expect(freePlanTitle).toBeInTheDocument(); expect(monthlyPlanTitle).toBeInTheDocument(); expect(yearlyPlanTitle).toBeInTheDocument(); - expect(fullAccessTitle).toBeInTheDocument(); + // expect(fullAccessTitle).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(2); fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); expect(emailInput).toHaveValue('jamie@example.com'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[0]); expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ email: 'jamie@example.com', @@ -286,8 +293,8 @@ describe('Signup', () => { describe('as paid member on single tier site', () => { test('with default settings on monthly plan', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, - siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle } = await setup({ site: FixtureSite.singleTier.basic }); @@ -300,9 +307,8 @@ describe('Signup', () => { expect(freePlanTitle).toBeInTheDocument(); expect(monthlyPlanTitle).toBeInTheDocument(); expect(yearlyPlanTitle).toBeInTheDocument(); - expect(fullAccessTitle).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(2); const monthlyPlanContainer = within(popupIframeDocument).queryByText(/Monthly$/); const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); @@ -316,19 +322,19 @@ describe('Signup', () => { await within(popupIframeDocument).findByText(benefitText); expect(emailInput).toHaveValue('jamie@example.com'); expect(nameInput).toHaveValue('Jamie Larsen'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[1]); expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ email: 'jamie@example.com', name: 'Jamie Larsen', offerId: undefined, - plan: singleTierProduct.monthlyPrice.id + plan: singleTierProduct.yearlyPrice.id }); }); test('with default settings on yearly plan', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, - siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle } = await setup({ site: FixtureSite.singleTier.basic }); @@ -341,9 +347,8 @@ describe('Signup', () => { expect(freePlanTitle).toBeInTheDocument(); expect(monthlyPlanTitle).toBeInTheDocument(); expect(yearlyPlanTitle).toBeInTheDocument(); - expect(fullAccessTitle).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(2); const yearlyPlanContainer = within(popupIframeDocument).queryByText(/Yearly$/); const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); @@ -357,7 +362,7 @@ describe('Signup', () => { await within(popupIframeDocument).findByText(benefitText); expect(emailInput).toHaveValue('jamie@example.com'); expect(nameInput).toHaveValue('Jamie Larsen'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[1]); expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ email: 'jamie@example.com', name: 'Jamie Larsen', @@ -370,8 +375,8 @@ describe('Signup', () => { test('without name field on monthly plan', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, - siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle } = await setup({ site: FixtureSite.singleTier.withoutName }); @@ -388,17 +393,16 @@ describe('Signup', () => { expect(freePlanTitle).toBeInTheDocument(); expect(monthlyPlanTitle).toBeInTheDocument(); expect(yearlyPlanTitle).toBeInTheDocument(); - expect(fullAccessTitle).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(2); fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); - fireEvent.click(monthlyPlanContainer.parentNode); + fireEvent.click(monthlyPlanContainer); await within(popupIframeDocument).findByText(benefitText); expect(emailInput).toHaveValue('jamie@example.com'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[1]); expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ email: 'jamie@example.com', @@ -410,7 +414,7 @@ describe('Signup', () => { test('with only paid plans available', async () => { let { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, siteTitle, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle } = await setup({ site: FixtureSite.singleTier.onlyPaidPlan @@ -425,7 +429,7 @@ describe('Signup', () => { expect(monthlyPlanTitle).toBeInTheDocument(); expect(yearlyPlanTitle).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(1); fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); @@ -433,14 +437,14 @@ describe('Signup', () => { expect(emailInput).toHaveValue('jamie@example.com'); expect(nameInput).toHaveValue('Jamie Larsen'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[0]); const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ email: 'jamie@example.com', name: 'Jamie Larsen', offerId: undefined, - plan: singleTierProduct.monthlyPrice.id + plan: singleTierProduct.yearlyPrice.id }); }); @@ -524,7 +528,7 @@ describe('Signup', () => { describe('as free member on multi tier site', () => { test('with default settings', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, siteTitle, popupIframeDocument, freePlanTitle } = await multiTierSetup({ site: FixtureSite.multipleTiers.basic @@ -537,14 +541,14 @@ describe('Signup', () => { expect(nameInput).toBeInTheDocument(); expect(freePlanTitle[0]).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(4); fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); expect(emailInput).toHaveValue('jamie@example.com'); expect(nameInput).toHaveValue('Jamie Larsen'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[0]); expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ email: 'jamie@example.com', name: 'Jamie Larsen', @@ -556,7 +560,7 @@ describe('Signup', () => { test('without name field', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, siteTitle, popupIframeDocument, freePlanTitle } = await multiTierSetup({ site: FixtureSite.multipleTiers.withoutName @@ -569,12 +573,11 @@ describe('Signup', () => { expect(nameInput).not.toBeInTheDocument(); expect(freePlanTitle[0]).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); expect(emailInput).toHaveValue('jamie@example.com'); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[0]); expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ email: 'jamie@example.com', @@ -627,7 +630,7 @@ describe('Signup', () => { describe('as paid member on multi tier site', () => { test('with default settings', async () => { const { - ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, siteTitle, popupIframeDocument, freePlanTitle } = await multiTierSetup({ site: FixtureSite.multipleTiers.basic @@ -645,7 +648,7 @@ describe('Signup', () => { expect(nameInput).toBeInTheDocument(); expect(freePlanTitle[0]).toBeInTheDocument(); expect(signinButton).toBeInTheDocument(); - expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(4); fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); @@ -661,7 +664,7 @@ describe('Signup', () => { // added fake timeout for react state delay in setting plan await new Promise(r => setTimeout(r, 10)); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[1]); await waitFor(() => expect(ghostApi.member.checkoutPlan).toHaveBeenCalledTimes(1)); }); diff --git a/ghost/portal/src/tests/UpgradeFlow.test.js b/ghost/portal/src/tests/UpgradeFlow.test.js index c8bf98fd8b..a8b10f2f33 100644 --- a/ghost/portal/src/tests/UpgradeFlow.test.js +++ b/ghost/portal/src/tests/UpgradeFlow.test.js @@ -34,12 +34,16 @@ const offerSetup = async ({site, member = null, offer}) => { const popupFrame = await utils.findByTitle(/portal-popup/i); const triggerButtonFrame = utils.queryByTitle(/portal-trigger/i); const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); const siteTitle = within(popupIframeDocument).queryByText(site.title); - const offerName = within(popupIframeDocument).queryByText(offer.name); + + const offerName = within(popupIframeDocument).queryByText(offer.display_title); + const offerDescription = within(popupIframeDocument).queryByText(offer.display_description); const freePlanTitle = within(popupIframeDocument).queryByText('Free'); @@ -56,6 +60,7 @@ const offerSetup = async ({site, member = null, offer}) => { nameInput, signinButton, submitButton, + chooseBtns, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, @@ -197,8 +202,12 @@ describe('Logged-in free member', () => { const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); fireEvent.click(viewPlansButton); - await within(popupIframeDocument).findByText('Monthly'); - const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const monthlyPlanContainer = await within(popupIframeDocument).findByText('Monthly'); + fireEvent.click(monthlyPlanContainer); + // added fake timeout for react state delay in setting plan + await new Promise(r => setTimeout(r, 10)); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Choose'}); fireEvent.click(submitButton); expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ @@ -229,11 +238,11 @@ describe('Logged-in free member', () => { fireEvent.click(viewPlansButton); await within(popupIframeDocument).findByText('Monthly'); const yearlyPlanContainer = await within(popupIframeDocument).findByText('Yearly'); - fireEvent.click(yearlyPlanContainer.parentNode); + fireEvent.click(yearlyPlanContainer); // added fake timeout for react state delay in setting plan await new Promise(r => setTimeout(r, 10)); - const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Choose'}); fireEvent.click(submitButton); expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ @@ -347,9 +356,9 @@ describe('Logged-in free member', () => { // allow default checkbox switch to yearly await new Promise(r => setTimeout(r, 10)); - const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); - fireEvent.click(submitButton); + fireEvent.click(chooseBtns[0]); expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ metadata: { checkoutType: 'upgrade' diff --git a/ghost/portal/src/tests/data-attributes.test.js b/ghost/portal/src/tests/data-attributes.test.js index 8450983da2..cbaf229a32 100644 --- a/ghost/portal/src/tests/data-attributes.test.js +++ b/ghost/portal/src/tests/data-attributes.test.js @@ -303,7 +303,7 @@ describe('Portal Data attributes:', () => { fireEvent.click(portalElement); popupFrame = await utils.findByTitle(/portal-popup/i); expect(popupFrame).toBeInTheDocument(); - const loginTitle = within(popupFrame.contentDocument).queryByText(/log in/i); + const loginTitle = within(popupFrame.contentDocument).queryByText(/sign in/i); expect(loginTitle).toBeInTheDocument(); }); }); diff --git a/ghost/portal/src/tests/portal-links.test.js b/ghost/portal/src/tests/portal-links.test.js index dedba92538..e307b198ed 100644 --- a/ghost/portal/src/tests/portal-links.test.js +++ b/ghost/portal/src/tests/portal-links.test.js @@ -116,7 +116,7 @@ describe('Portal Data links:', () => { expect(triggerButtonFrame).toBeInTheDocument(); popupFrame = await utils.findByTitle(/portal-popup/i); expect(popupFrame).toBeInTheDocument(); - const loginTitle = within(popupFrame.contentDocument).queryByText(/log in/i); + const loginTitle = within(popupFrame.contentDocument).queryByText(/sign in/i); expect(loginTitle).toBeInTheDocument(); }); }); diff --git a/ghost/portal/src/utils/fixtures-generator.js b/ghost/portal/src/utils/fixtures-generator.js index d5df27a7a1..d45b9dc912 100644 --- a/ghost/portal/src/utils/fixtures-generator.js +++ b/ghost/portal/src/utils/fixtures-generator.js @@ -66,8 +66,8 @@ export function getSiteData({ export function getOfferData({ name = 'Black Friday', code = 'black-friday', - displayTitle = 'Black Friday', - displayDescription = 'Special deal', + displayTitle = 'Black Friday Sale!', + displayDescription = 'Special deal for Black Friday. Subscribe now for only $15 per month and get additional benefits like accessing our podcast.', type = 'percent', cadence = 'month', amount = 50, diff --git a/ghost/portal/src/utils/fixtures.js b/ghost/portal/src/utils/fixtures.js index a4cd3de0b1..a4a78783c0 100644 --- a/ghost/portal/src/utils/fixtures.js +++ b/ghost/portal/src/utils/fixtures.js @@ -8,7 +8,7 @@ const products = [ name: 'Free', description: 'Free tier description which is actually a pretty long description', // description: '', - numOfBenefits: 0 + numOfBenefits: 2 }) , getProductData({ @@ -17,13 +17,13 @@ const products = [ description: '', monthlyPrice: getPriceData({ interval: 'month', - amount: 700 + amount: 500 }), yearlyPrice: getPriceData({ interval: 'year', - amount: 7000 + amount: 5000 }), - numOfBenefits: 1 + numOfBenefits: 3 }) , getProductData({ @@ -37,9 +37,24 @@ const products = [ interval: 'year', amount: 11000 }), - numOfBenefits: 1 + numOfBenefits: 4 }) + // , + // getProductData({ + // name: 'Silver', + // description: 'Access to all members articles and weekly podcast', + // monthlyPrice: getPriceData({ + // interval: 'month', + // amount: 1200 + // }), + // yearlyPrice: getPriceData({ + // interval: 'year', + // amount: 12000 + // }), + // numOfBenefits: 3 + // }) + // // , // getProductData({ // name: 'Friends of the Blueprint', @@ -146,7 +161,10 @@ export const member = { }; export function paidMemberOnTier() { - let price = site?.products?.[2].monthlyPrice; + if (!products || !products[1]) { + return null; + } + let price = site?.products?.[1].monthlyPrice; let updatedMember = getMemberData({ paid: true, subscriptions: [ diff --git a/ghost/portal/src/utils/helpers.js b/ghost/portal/src/utils/helpers.js index feacceb3d3..bfbc4da2ad 100644 --- a/ghost/portal/src/utils/helpers.js +++ b/ghost/portal/src/utils/helpers.js @@ -297,7 +297,7 @@ export function getSiteProducts({site, pageQuery}) { if (showOnlyFree) { return []; } - if (hasFreeProductPrice({site}) && products.length > 0) { + if (hasFreeProductPrice({site})) { products.unshift({ id: 'free' }); @@ -311,7 +311,11 @@ export function getFreeProductBenefits({site}) { } export function getFreeTierTitle({site}) { - return 'Free'; + if (hasOnlyFreeProduct({site})) { + return 'Free membership'; + } else { + return 'Free'; + } } export function getFreeTierDescription({site}) { @@ -369,6 +373,11 @@ export function hasFreeProductPrice({site}) { return allowSelfSignup && portalPlans.includes('free'); } +export function hasOnlyFreeProduct({site}) { + const products = getSiteProducts({site}); + return (products.length === 1 && hasFreeProductPrice({site})); +} + export function getProductFromPrice({site, priceId}) { if (priceId === 'free') { return getFreeProduct({site});