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:
parent
186f8a7f81
commit
76727b28e4
5 changed files with 169 additions and 78 deletions
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
55
apps/admin-x-settings/src/utils/getOffersPortalPreviewUrl.ts
Normal file
55
apps/admin-x-settings/src/utils/getOffersPortalPreviewUrl.ts
Normal 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()}`;
|
||||
};
|
61
apps/admin-x-settings/src/utils/getPortalPreviewUrl.ts
Normal file
61
apps/admin-x-settings/src/utils/getPortalPreviewUrl.ts
Normal 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()}`;
|
||||
};
|
Loading…
Add table
Reference in a new issue