0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Added Portal Preview for Offers (#18905)

refs
https://www.notion.so/ghost/Integrate-Portal-Offers-preview-920d06c82eb94dba9b7eaabfa02c4e26

- First phase of implementing the offers portal preview, we refactor the
existing portal renderer so we can keep things more dry and easier to
integrate.
- We add a `getOfferPortalPreviewUrl` to generate the URL.

---

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

This pull request adds and refactors some components and utility
functions to improve the portal preview feature for membership offers.
It introduces a `PortalFrame` component to show a live preview of an
offer in the `AddOfferModal` component, and a `getOfferPortalPreviewUrl`
function to generate the preview URL. It also extracts the existing
`getPortalPreviewUrl` function from the `PortalFrame` component to a
separate file, and simplifies the props and logic of the `PortalPreview`
and `PortalFrame` components.
This commit is contained in:
Ronald Langeveld 2023-11-08 12:54:38 +07:00 committed by GitHub
parent 186f8a7f81
commit 76727b28e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 78 deletions

View file

@ -1,5 +1,7 @@
import Form from '../../../../admin-x-ds/global/form/Form';
// import IframeBuffering from '../../../../utils/IframeBuffering';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import PortalFrame from '../portal/PortalFrame';
import Select from '../../../../admin-x-ds/global/form/Select';
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
import TextField from '../../../../admin-x-ds/global/form/TextField';
@ -7,6 +9,8 @@ import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useRouting from '../../../../hooks/useRouting';
import {ReactComponent as CheckIcon} from '../../../../admin-x-ds/assets/icons/check.svg';
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
// import {getActiveTiers, useBrowseTiers} from '../../../../api/tiers';
import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl';
import {useEffect} from 'react';
interface OfferType {
@ -125,6 +129,8 @@ const AddOfferModal = () => {
const modal = useModal();
const {updateRoute} = useRouting();
const hasOffers = useFeatureFlag('adminXOffers');
// const {data: {tiers, meta, isEnd} = {}} = useBrowseTiers();
// const activeTiers = getActiveTiers(tiers || []);
useEffect(() => {
if (!hasOffers) {
@ -139,8 +145,31 @@ const AddOfferModal = () => {
};
const sidebar = <Sidebar />;
// TODO: wire up the data from the sidebar inputs
let overrides : offerPortalPreviewUrlTypes = {
disableBackground: true,
name: 'Black Friday',
code: 'black-friday',
displayTitle: 'Black Friday Special',
displayDescription: 'Take advantage of this limited-time offer.',
type: 'discount',
cadence: 'monthly',
amount: 1200,
duration: '',
durationInMonths: 12,
currency: 'USD',
status: 'active',
tierId: ''
};
return <PreviewModalContent cancelLabel='Cancel' deviceSelector={false} okLabel='Publish' sidebar={sidebar} size='full' title='Offer' onCancel={cancelAddOffer} />;
const href = getOfferPortalPreviewUrl(overrides, 'http://localhost:2368');
const iframe = <PortalFrame
href={href}
/>;
return <PreviewModalContent cancelLabel='Cancel' deviceSelector={false} okLabel='Publish' preview={iframe} sidebar={sidebar} size='full' title='Offer' onCancel={cancelAddOffer} />;
};
export default NiceModal.create(AddOfferModal);

View file

@ -1,88 +1,19 @@
import React, {useEffect, useRef, useState} from 'react';
import {Config} from '../../../../api/config';
import {Setting, checkStripeEnabled, getSettingValue} from '../../../../api/settings';
import {SiteData} from '../../../../api/site';
import {Tier} from '../../../../api/tiers';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
type PortalFrameProps = {
settings: Setting[];
tiers: Tier[];
selectedTab: string;
selectedTab?: string;
href: string;
}
function getPortalPreviewUrl({settings, config, tiers, siteData, selectedTab}: {
settings: Setting[];
config: Config;
tiers: Tier[];
siteData: SiteData | null;
selectedTab: string;
}) {
if (!siteData?.url) {
return null;
// we should refactor this to be reused in offers as well
const PortalFrame: React.FC<PortalFrameProps> = ({selectedTab, href}) => {
if (!selectedTab) {
selectedTab = 'signup';
}
let portalTiers = tiers.filter((t) => {
return t.visibility === 'public' && t.type === 'paid';
}).map(t => t.id);
const baseUrl = siteData.url.replace(/\/$/, '');
const portalBase = '/?v=modal-portal-settings#/portal/preview';
const portalPlans: string[] = JSON.parse(getSettingValue<string>(settings, 'portal_plans') || '');
const membersSignupAccess = getSettingValue<string>(settings, 'members_signup_access');
const allowSelfSignup = membersSignupAccess === 'all' && (!checkStripeEnabled(settings, config) || portalPlans.includes('free'));
const settingsParam = new URLSearchParams();
settingsParam.append('button', getSettingValue(settings, 'portal_button') ? 'true' : 'false');
settingsParam.append('name', getSettingValue(settings, 'portal_name') ? 'true' : 'false');
settingsParam.append('isFree', portalPlans.includes('free') ? 'true' : 'false');
settingsParam.append('isMonthly', checkStripeEnabled(settings, config) && portalPlans.includes('monthly') ? 'true' : 'false');
settingsParam.append('isYearly', checkStripeEnabled(settings, config) && portalPlans.includes('yearly') ? 'true' : 'false');
settingsParam.append('page', selectedTab === 'account' ? 'accountHome' : 'signup');
settingsParam.append('buttonIcon', encodeURIComponent(getSettingValue(settings, 'portal_button_icon') || 'icon-1'));
settingsParam.append('signupButtonText', encodeURIComponent(getSettingValue(settings, 'portal_button_signup_text') || ''));
settingsParam.append('membersSignupAccess', getSettingValue(settings, 'members_signup_access') || 'all');
settingsParam.append('allowSelfSignup', allowSelfSignup ? 'true' : 'false');
settingsParam.append('signupTermsHtml', getSettingValue(settings, 'portal_signup_terms_html') || '');
settingsParam.append('signupCheckboxRequired', getSettingValue(settings, 'portal_signup_checkbox_required') ? 'true' : 'false');
settingsParam.append('portalProducts', encodeURIComponent(portalTiers.join(','))); // assuming that it might be more than 1
if (portalPlans && portalPlans.length) {
settingsParam.append('portalPrices', encodeURIComponent(portalPlans.join(',')));
}
const accentColor = getSettingValue(settings, 'accent_color');
if (accentColor !== undefined && accentColor !== null) {
settingsParam.append('accentColor', encodeURIComponent(accentColor));
}
const portalButtonStyle = getSettingValue(settings, 'portal_button_style');
if (portalButtonStyle) {
settingsParam.append('buttonStyle', encodeURIComponent(portalButtonStyle));
}
settingsParam.append('disableBackground', 'false');
return `${baseUrl}${portalBase}?${settingsParam.toString()}`;
}
const PortalFrame: React.FC<PortalFrameProps> = ({settings, tiers, selectedTab}) => {
const {
siteData,
config
} = useGlobalData();
const iframeRef = useRef<HTMLIFrameElement>(null);
const [portalReady, setPortalReady] = useState(false);
let href = getPortalPreviewUrl({
settings,
config,
tiers,
siteData,
selectedTab
});
useEffect(() => {
const messageListener = (event: MessageEvent<'portal-ready' | {type: string}>) => {
if (!href) {

View file

@ -3,6 +3,8 @@ import PortalLinks from './PortalLinks';
import React from 'react';
import {Setting} from '../../../../api/settings';
import {Tier} from '../../../../api/tiers';
import {getPortalPreviewUrl} from '../../../../utils/getPortalPreviewUrl';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
interface PortalPreviewProps {
selectedTab: string;
@ -15,13 +17,26 @@ const PortalPreview: React.FC<PortalPreviewProps> = ({
localSettings,
localTiers
}) => {
const {
siteData,
config
} = useGlobalData();
const href = getPortalPreviewUrl({
settings: localSettings,
tiers: localTiers,
selectedTab,
siteData,
config
});
let tabContents = <></>;
switch (selectedTab) {
case 'account':
tabContents = (
<>
<PortalFrame selectedTab={selectedTab} settings={localSettings} tiers={localTiers} />
<PortalFrame href={href || ''} selectedTab={selectedTab} />
</>
);
break;
@ -31,7 +46,7 @@ const PortalPreview: React.FC<PortalPreviewProps> = ({
default:
tabContents = (
<>
<PortalFrame selectedTab={selectedTab} settings={localSettings} tiers={localTiers} />
<PortalFrame href={href || ''} selectedTab={selectedTab} />
</>
);
break;

View file

@ -0,0 +1,55 @@
export type offerPortalPreviewUrlTypes = {
disableBackground?: boolean;
name: string;
code: string;
displayTitle?: string;
displayDescription?: string;
type: string;
cadence: string;
amount?: number;
duration: string;
durationInMonths: number;
currency?: string;
status: string;
tierId: string;
};
export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, baseUrl: string) : string => {
const {
disableBackground = false,
name,
code,
displayTitle = '',
displayDescription = '',
type,
cadence,
amount = 0,
duration,
durationInMonths,
currency = 'usd',
status,
tierId
} = overrides;
const portalBase = '/#/portal/preview/offer';
const settingsParam = new URLSearchParams();
settingsParam.append('name', encodeURIComponent(name));
settingsParam.append('code', encodeURIComponent(code));
settingsParam.append('display_title', encodeURIComponent(displayTitle));
settingsParam.append('display_description', encodeURIComponent(displayDescription));
settingsParam.append('type', encodeURIComponent(type));
settingsParam.append('cadence', encodeURIComponent(cadence));
settingsParam.append('amount', encodeURIComponent(amount));
settingsParam.append('duration', encodeURIComponent(duration));
settingsParam.append('duration_in_months', encodeURIComponent(durationInMonths));
settingsParam.append('currency', encodeURIComponent(currency));
settingsParam.append('status', encodeURIComponent(status));
settingsParam.append('tier_id', encodeURIComponent(tierId));
if (disableBackground) {
settingsParam.append('disableBackground', 'true');
}
return `${baseUrl}${portalBase}?${settingsParam.toString()}`;
};

View file

@ -0,0 +1,61 @@
import {Config} from '../api/config';
import {Setting, checkStripeEnabled, getSettingValue} from '../api/settings';
import {SiteData} from '../api/site';
import {Tier} from '../api/tiers';
export type portalPreviewUrlTypes = {
settings: Setting[];
config: Config;
tiers: Tier[];
siteData: SiteData | null;
selectedTab: string;
};
export const getPortalPreviewUrl = ({settings, config, tiers, siteData, selectedTab} : portalPreviewUrlTypes): string | null => {
if (!siteData?.url) {
return null;
}
let portalTiers = tiers.filter((t) => {
return t.visibility === 'public' && t.type === 'paid';
}).map(t => t.id);
const baseUrl = siteData.url.replace(/\/$/, '');
const portalBase = '/?v=modal-portal-settings#/portal/preview';
const portalPlans: string[] = JSON.parse(getSettingValue<string>(settings, 'portal_plans') || '');
const membersSignupAccess = getSettingValue<string>(settings, 'members_signup_access');
const allowSelfSignup = membersSignupAccess === 'all' && (!checkStripeEnabled(settings, config) || portalPlans.includes('free'));
const settingsParam = new URLSearchParams();
settingsParam.append('button', getSettingValue(settings, 'portal_button') ? 'true' : 'false');
settingsParam.append('name', getSettingValue(settings, 'portal_name') ? 'true' : 'false');
settingsParam.append('isFree', portalPlans.includes('free') ? 'true' : 'false');
settingsParam.append('isMonthly', checkStripeEnabled(settings, config) && portalPlans.includes('monthly') ? 'true' : 'false');
settingsParam.append('isYearly', checkStripeEnabled(settings, config) && portalPlans.includes('yearly') ? 'true' : 'false');
settingsParam.append('page', selectedTab === 'account' ? 'accountHome' : 'signup');
settingsParam.append('buttonIcon', encodeURIComponent(getSettingValue(settings, 'portal_button_icon') || 'icon-1'));
settingsParam.append('signupButtonText', encodeURIComponent(getSettingValue(settings, 'portal_button_signup_text') || ''));
settingsParam.append('membersSignupAccess', getSettingValue(settings, 'members_signup_access') || 'all');
settingsParam.append('allowSelfSignup', allowSelfSignup ? 'true' : 'false');
settingsParam.append('signupTermsHtml', getSettingValue(settings, 'portal_signup_terms_html') || '');
settingsParam.append('signupCheckboxRequired', getSettingValue(settings, 'portal_signup_checkbox_required') ? 'true' : 'false');
settingsParam.append('portalProducts', encodeURIComponent(portalTiers.join(','))); // assuming that it might be more than 1
if (portalPlans && portalPlans.length) {
settingsParam.append('portalPrices', encodeURIComponent(portalPlans.join(',')));
}
const accentColor = getSettingValue(settings, 'accent_color');
if (accentColor !== undefined && accentColor !== null) {
settingsParam.append('accentColor', encodeURIComponent(accentColor));
}
const portalButtonStyle = getSettingValue(settings, 'portal_button_style');
if (portalButtonStyle) {
settingsParam.append('buttonStyle', encodeURIComponent(portalButtonStyle));
}
settingsParam.append('disableBackground', 'false');
return `${baseUrl}${portalBase}?${settingsParam.toString()}`;
};