Fixed Offers portal preview edge cases (#19124)

no issue

- cleaned up offers portal preview.
- fixes a few logic errors and potential edge cases and making it easier
to maintain.
This commit is contained in:
Ronald Langeveld 2023-11-24 12:32:45 +02:00 committed by GitHub
parent 819ddccc72
commit 41ee387af2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 182 additions and 108 deletions

View File

@ -5,7 +5,7 @@ import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../
import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
import {getTiersCadences} from '../../../../utils/getTiersCadences';
import {useAddOffer} from '@tryghost/admin-x-framework/api/offers';
import {useEffect, useState} from 'react';
import {useEffect, useMemo, useState} from 'react';
import {useForm} from '@tryghost/admin-x-framework/hooks';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useModal} from '@ebay/nice-modal-react';
@ -45,12 +45,52 @@ const ButtonSelect: React.FC<{type: OfferType, checked: boolean, onClick: () =>
);
};
type formStateTypes = {
disableBackground?: boolean;
name: string;
code: {
isDirty: boolean;
value: string;
};
displayTitle: {
isDirty: boolean;
value: string;
};
displayDescription: string;
type: string;
cadence: string;
amount: number;
duration: string;
durationInMonths: number;
currency: string;
status: string;
tierId: string;
fixedAmount?: number;
trialAmount?: number;
percentAmount?: number;
};
const calculateAmount = (formState: formStateTypes): number => {
const {fixedAmount = 0, percentAmount = 0, trialAmount = 0, amount = 0} = formState;
switch (formState.type) {
case 'fixed':
return fixedAmount * 100;
case 'percent':
return percentAmount;
case 'trial':
return trialAmount;
default:
return amount;
}
};
type SidebarProps = {
tierOptions: SelectOption[];
handleTierChange: (tier: SelectOption) => void;
selectedTier: SelectOption;
overrides: offerPortalPreviewUrlTypes
handleTextInput: (e: React.ChangeEvent<HTMLInputElement>, key: keyof offerPortalPreviewUrlTypes) => void;
overrides: formStateTypes;
// handleTextInput: (e: React.ChangeEvent<HTMLInputElement>, key: keyof offerPortalPreviewUrlTypes) => void;
amountOptions: SelectOption[];
typeOptions: OfferType[];
durationOptions: SelectOption[];
@ -59,12 +99,16 @@ type SidebarProps = {
handleAmountTypeChange: (amountType: string) => void;
handleNameInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleTextAreaInput: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleDisplayTitleInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleAmountInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleDurationInMonthsInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleCodeInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
const Sidebar: React.FC<SidebarProps> = ({tierOptions,
handleTierChange,
selectedTier,
handleTextInput,
// handleTextInput,
typeOptions,
durationOptions,
handleTypeChange,
@ -73,6 +117,10 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
handleAmountTypeChange,
handleNameInput,
handleTextAreaInput,
handleDisplayTitleInput,
handleDurationInMonthsInput,
handleAmountInput,
handleCodeInput,
amountOptions}) => {
const getFilteredDurationOptions = () => {
// Check if the selected tier's cadence is 'yearly'
@ -117,15 +165,15 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
/>
{
overrides.type !== 'trial' && <> <div className='relative'>
<TextField title='Amount off' type='number' onChange={(e) => {
handleTextInput(e, 'discountAmount');
<TextField title='Amount off' type='number' value={overrides.type === 'fixed' ? overrides.fixedAmount?.toString() : overrides.percentAmount?.toString()} onChange={(e) => {
handleAmountInput(e);
}} />
<div className='absolute bottom-0 right-1.5 z-10'>
<Select
clearBg={true}
controlClasses={{menu: 'w-20 right-0'}}
options={amountOptions}
selectedOption={overrides.amountType === 'percent' ? amountOptions[0] : amountOptions[1]}
selectedOption={overrides.type === 'percent' ? amountOptions[0] : amountOptions[1]}
onSelect={(e) => {
handleAmountTypeChange(e?.value || '');
}}
@ -141,7 +189,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
{
overrides.duration === 'repeating' && <TextField title='Duration in months' type='number' onChange={(e) => {
handleTextInput(e, 'durationInMonths');
handleDurationInMonthsInput(e);
}} />
}
</>
@ -149,7 +197,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
{
overrides.type === 'trial' && <TextField title='Trial duration' type='number' value={overrides.trialAmount?.toString()} onChange={(e) => {
handleTextInput(e, 'trialAmount');
handleAmountInput(e);
}} />
}
@ -163,7 +211,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
title='Display title'
value={overrides.displayTitle.value}
onChange={(e) => {
handleTextInput(e, 'displayTitle');
handleDisplayTitleInput(e);
}}
/>
<TextField
@ -171,7 +219,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
title='Offer code'
value={overrides.code.value}
onChange={(e) => {
handleTextInput(e, 'code');
handleCodeInput(e);
}}
/>
<TextArea
@ -226,19 +274,22 @@ const AddOfferModal = () => {
currency: tierCadenceOptions[0]?.value ? parseData(tierCadenceOptions[0]?.value).currency : ''
}
});
const getDiscountAmount = (discount: number, dctype: string) => {
if (dctype === 'percent') {
return discount.toString();
}
if (dctype === 'fixed') {
let calcDiscount = discount * 100;
return calcDiscount.toString();
}
};
// const calculateAmount = useCallback(() => {
// if (formState.type === 'fixed') {
// return formState.fixedAmount;
// } else if (formState.type === 'percent') {
// return formState.percentAmount;
// } else if (formState.type === 'trial') {
// return formState.trialAmount;
// } else {
// return formState.amount; // default case
// }
// }, [f;
const {formState, updateForm, handleSave, saveState, okProps} = useForm({
initialState: {
disableBackground: true,
disableBackground: false,
name: '',
code: {
isDirty: false,
@ -251,14 +302,15 @@ const AddOfferModal = () => {
displayDescription: '',
type: 'percent',
cadence: selectedTier?.dataset?.period || '',
trialAmount: 7,
discountAmount: 0,
amount: 0,
duration: 'once',
durationInMonths: 0,
currency: selectedTier?.dataset?.currency || '',
currency: selectedTier?.dataset?.currency || 'USD',
status: 'active',
tierId: selectedTier?.dataset?.id || '',
amountType: 'percent'
trialAmount: 7,
fixedAmount: 0,
percentAmount: 0
},
onSave: async () => {
const dataset = {
@ -267,7 +319,7 @@ const AddOfferModal = () => {
display_title: formState.displayTitle.value,
display_description: formState.displayDescription,
cadence: formState.cadence,
amount: formState.type === 'trial' ? Number(getDiscountAmount(formState.trialAmount, formState.amountType)) : Number(getDiscountAmount(formState.discountAmount, formState.amountType)),
amount: calculateAmount(formState) || 0,
duration: formState.type === 'trial' ? 'trial' : formState.duration,
duration_in_months: Number(formState.durationInMonths),
currency: formState.currency,
@ -320,39 +372,37 @@ const AddOfferModal = () => {
const handleAmountTypeChange = (amountType: string) => {
updateForm(state => ({
...state,
amountType: amountType,
type: amountType === 'percent' ? 'percent' : 'fixed' || state.type
}));
};
const handleTextInput = (
e: React.ChangeEvent<HTMLInputElement>,
key: keyof offerPortalPreviewUrlTypes
) => {
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
updateForm((state) => {
// Extract the current value for the key
const currentValue = (state as offerPortalPreviewUrlTypes)[key];
// Check if the current value is an object and has 'isDirty' and 'value' properties
if (currentValue && typeof currentValue === 'object' && 'isDirty' in currentValue && 'value' in currentValue) {
// Determine if the field has been modified
const handleAmountInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
return {
...state,
[key]: {
...currentValue,
isDirty: true,
value: target.value
}
};
} else {
// For simple properties, update the value directly
return {
...state,
[key]: target.value
};
}
});
if (formState.type === 'fixed') {
updateForm(state => ({
...state,
fixedAmount: Number(target.value)
}));
} else if (formState.type === 'percent') {
updateForm(state => ({
...state,
percentAmount: Number(target.value)
}));
} else {
updateForm(state => ({
...state,
amount: Number(target.value)
}));
}
};
const handleDurationInMonthsInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
updateForm(state => ({
...state,
durationInMonths: Number(target.value)
}));
};
const handleNameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -376,6 +426,18 @@ const AddOfferModal = () => {
});
};
const handleDisplayTitleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
updateForm(state => ({
...state,
displayTitle: {
...state.displayTitle,
isDirty: true,
value: target.value
}
}));
};
const handleTextAreaInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const target = e.target as HTMLTextAreaElement;
updateForm(state => ({
@ -391,6 +453,18 @@ const AddOfferModal = () => {
}));
};
const handleCodeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
updateForm(state => ({
...state,
code: {
...state.code,
isDirty: true,
value: target.value
}
}));
};
useEffect(() => {
if (!hasOffers) {
modal.remove();
@ -402,19 +476,54 @@ const AddOfferModal = () => {
updateRoute('offers/edit');
};
// const overrides : offerPortalPreviewUrlTypes = {
// name: formState.name,
// code: formState.code.value,
// displayTitle: formState.displayTitle.value,
// displayDescription: formState.displayDescription,
// type: formState.type,
// cadence: formState.cadence,
// amount: calculateAmount(),
// duration: formState.type === 'trial' ? 'trial' : formState.duration,
// durationInMonths: formState.durationInMonths,
// currency: formState.currency,
// status: formState.status,
// tierId: formState.tierId
// };
const overrides : offerPortalPreviewUrlTypes = useMemo(() => {
return {
name: formState.name,
code: formState.code.value,
displayTitle: formState.displayTitle.value,
displayDescription: formState.displayDescription,
type: formState.type,
cadence: formState.cadence,
amount: calculateAmount(formState) || 0,
duration: formState.type === 'trial' ? 'trial' : formState.duration,
durationInMonths: formState.durationInMonths,
currency: formState.currency,
status: formState.status,
tierId: formState.tierId
};
}, [formState]);
useEffect(() => {
const newHref = getOfferPortalPreviewUrl(formState, siteData.url);
const newHref = getOfferPortalPreviewUrl(overrides, siteData.url);
setHref(newHref);
}, [formState, siteData.url]);
}, [formState, siteData.url, formState.type, overrides]);
const sidebar = <Sidebar
amountOptions={amountOptions as SelectOption[]}
durationOptions={durationOptions}
handleAmountInput={handleAmountInput}
handleAmountTypeChange={handleAmountTypeChange}
handleCodeInput={handleCodeInput}
handleDisplayTitleInput={handleDisplayTitleInput}
handleDurationChange={handleDurationChange}
handleDurationInMonthsInput={handleDurationInMonthsInput}
handleNameInput={handleNameInput}
handleTextAreaInput={handleTextAreaInput}
handleTextInput={handleTextInput}
handleTierChange={handleTierChange}
handleTypeChange={handleTypeChange}
overrides={formState}

View File

@ -237,23 +237,17 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => {
useEffect(() => {
const dataset : offerPortalPreviewUrlTypes = {
name: formState?.name || '',
code: {
value: formState?.code || ''
},
displayTitle: {
value: formState?.display_title || ''
},
code: formState?.code || '',
displayTitle: formState?.display_title || '',
displayDescription: formState?.display_description || '',
type: formState?.type || '',
cadence: formState?.cadence || '',
trialAmount: formState?.amount,
discountAmount: formState?.amount,
amount: formState?.amount,
duration: formState?.duration || '',
durationInMonths: formState?.duration_in_months || 0,
currency: formState?.currency || '',
status: formState?.status || '',
tierId: formState?.tier.id || '',
amountType: formState?.type === 'percent' ? 'percent' : 'amount'
tierId: formState?.tier.id || ''
};
const newHref = getOfferPortalPreviewUrl(dataset, siteData.url);

View File

@ -1,46 +1,29 @@
export type offerPortalPreviewUrlTypes = {
disableBackground?: boolean;
name: string;
code: {
isDirty?: boolean;
value: string;
}
displayTitle: {
isDirty?: boolean;
value: string;
}
displayDescription?: string;
code: string;
displayTitle: string;
displayDescription: string;
type: string;
cadence: string;
trialAmount?: number;
discountAmount?: number;
percentageOff?: number;
amount: number;
duration: string;
durationInMonths: number;
currency?: string;
currency: string;
status: string;
tierId: string;
amountType?: string;
};
export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, baseUrl: string) : string => {
const {
disableBackground = false,
name,
code = {
isDirty: false,
value: ''
},
displayTitle = {
isDirty: false,
value: ''
},
code,
displayTitle = '',
displayDescription = '',
type,
cadence,
trialAmount = 7,
discountAmount = 0,
amountType,
amount = 0,
duration,
durationInMonths,
currency = 'usd',
@ -51,30 +34,18 @@ export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, b
const portalBase = '/#/portal/preview/offer';
const settingsParam = new URLSearchParams();
settingsParam.append('type', encodeURIComponent(type));
const getDiscountAmount = (discount: number, dctype: string) => {
if (dctype === 'percent') {
return discount.toString();
}
if (dctype === 'fixed') {
settingsParam.append('type', encodeURIComponent('fixed'));
let calcDiscount = discount * 100;
return calcDiscount.toString();
}
};
settingsParam.append('name', encodeURIComponent(name));
settingsParam.append('code', encodeURIComponent(code.value));
settingsParam.append('display_title', encodeURIComponent(displayTitle.value));
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));
settingsParam.append('amount', encodeURIComponent(type === 'trial' ? trialAmount.toString() : getDiscountAmount(discountAmount, amountType ? amountType : 'fixed') || '0'));
if (disableBackground) {
settingsParam.append('disableBackground', 'true');