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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/admin-x-framework/src/api/offers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Meta, createMutation, createQuery, createQueryWithId} from '../utils/api/hooks';
import {updateQueryCache, insertToQueryCache} from '../utils/api/update-queries';
import {useQueryClient} from '@tanstack/react-query';

export type Offer = {
id: string;
Expand Down Expand Up @@ -44,6 +45,14 @@ export interface OfferAddResponseType {

const dataType = 'OffersResponseType';

export const useInvalidateOffers = () => {
const queryClient = useQueryClient();

return () => {
return queryClient.invalidateQueries([dataType]);
};
};

export const useBrowseOffers = createQuery<OffersResponseType>({
dataType,
path: '/offers/',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,8 @@ const AddOfferModal = () => {
durationInMonths: formState.durationInMonths || 0,
currency: formState.currency || 'USD',
status: formState.status || 'active',
tierId: formState.tierId || activeTiers[0]?.id
tierId: formState.tierId || activeTiers[0]?.id,
redemptionType: 'signup'
};
}, [formState, activeTiers]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => {
durationInMonths: formState?.duration_in_months || 0,
currency: formState?.currency || '',
status: formState?.status || '',
tierId: formState?.tier?.id || ''
tierId: formState?.tier?.id || '',
redemptionType: 'signup'
};

const newHref = getOfferPortalPreviewUrl(dataset, siteData.url);
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {type Offer, useBrowseOffers} from '@tryghost/admin-x-framework/api/offers';
import {useRouting} from '@tryghost/admin-x-framework/routing';

// TODO: Replace placeholder data with real retention offer data from API
type RetentionCadence = 'month' | 'year';

type RetentionOffer = {
id: string;
Expand All @@ -12,29 +13,98 @@ type RetentionOffer = {
status: 'active' | 'inactive';
};

const placeholderRetentionOffers: RetentionOffer[] = [
{
id: 'monthly',
name: 'Monthly retention',
description: 'Applied to monthly plans',
terms: '50% OFF',
termsDetail: 'Next payment',
redemptions: 3,
status: 'active'
},
{
id: 'yearly',
name: 'Yearly retention',
description: 'Applied to annual plans',
terms: null,
termsDetail: null,
redemptions: 0,
status: 'inactive'
const getActiveRetentionOfferByCadence = (offers: Offer[], cadence: RetentionCadence): Offer | null => {
return offers.find((offer) => {
return offer.redemption_type === 'retention' &&
offer.cadence === cadence &&
offer.status === 'active';
}) || null;
};

const getRetentionRedemptionsByCadence = (offers: Offer[], cadence: RetentionCadence): number => {
return offers.reduce((total, offer) => {
if (offer.redemption_type !== 'retention' || offer.cadence !== cadence) {
return total;
}

return total + (offer.redemption_count || 0);
}, 0);
};

const getRetentionTerms = (offer: Offer | null): string | null => {
if (!offer) {
return null;
}

if (offer.type === 'free_months') {
const monthLabel = offer.amount === 1 ? 'month' : 'months';
return `${offer.amount} ${monthLabel} free`;
}

if (offer.type === 'percent') {
return `${offer.amount}% OFF`;
}

return null;
};

const getRetentionTermsDetail = (offer: Offer | null): string | null => {
if (!offer) {
return null;
}
];

if (offer.type === 'free_months') {
return '';
}

if (offer.duration === 'once') {
return 'First payment';
}

if (offer.duration === 'repeating' && offer.duration_in_months) {
const monthLabel = offer.duration_in_months === 1 ? 'month' : 'months';
return `For ${offer.duration_in_months} ${monthLabel}`;
}

if (offer.duration === 'forever') {
return 'Forever';
}

return null;
};

const getRetentionOffers = (offers: Offer[]): RetentionOffer[] => {
const monthlyOffer = getActiveRetentionOfferByCadence(offers, 'month');
const yearlyOffer = getActiveRetentionOfferByCadence(offers, 'year');
const monthlyRedemptions = getRetentionRedemptionsByCadence(offers, 'month');
const yearlyRedemptions = getRetentionRedemptionsByCadence(offers, 'year');

return [
{
id: 'monthly',
name: 'Monthly retention',
description: 'Applied to monthly plans',
terms: getRetentionTerms(monthlyOffer),
termsDetail: getRetentionTermsDetail(monthlyOffer),
redemptions: monthlyRedemptions,
status: monthlyOffer ? 'active' : 'inactive'
},
{
id: 'yearly',
name: 'Yearly retention',
description: 'Applied to annual plans',
terms: getRetentionTerms(yearlyOffer),
termsDetail: getRetentionTermsDetail(yearlyOffer),
redemptions: yearlyRedemptions,
status: yearlyOffer ? 'active' : 'inactive'
}
];
};

const OffersRetention: React.FC = () => {
const {updateRoute} = useRouting();
const {data: {offers: allOffers = []} = {}} = useBrowseOffers();
const retentionOffers = getRetentionOffers(allOffers);

const handleRetentionOfferClick = (id: string) => {
updateRoute(`offers/edit/retention/${id}`);
Expand All @@ -50,7 +120,7 @@ const OffersRetention: React.FC = () => {
<col className='w-[220px]' />
<col className='w-[80px]' />
</colgroup>
{placeholderRetentionOffers.map(offer => (
{retentionOffers.map(offer => (
<tr key={offer.id} className='group relative scale-100 border-b border-b-grey-200 dark:border-grey-800' data-testid='retention-offer-item'>
<td className='p-0'>
<a className='block cursor-pointer p-5 pl-0' onClick={() => handleRetentionOfferClick(offer.id)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type offerPortalPreviewUrlTypes = {
currency: string;
status: string;
tierId: string;
redemptionType: 'signup' | 'retention';
};

export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, baseUrl: string) : string => {
Expand All @@ -28,7 +29,8 @@ export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, b
durationInMonths,
currency = 'usd',
status,
tierId
tierId,
redemptionType = 'signup'
} = overrides;

baseUrl = baseUrl.replace(/\/$/, '');
Expand All @@ -48,6 +50,7 @@ export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, b
settingsParam.append('currency', encodeURIComponent(currency));
settingsParam.append('status', encodeURIComponent(status));
settingsParam.append('tier_id', encodeURIComponent(tierId));
settingsParam.append('redemption_type', encodeURIComponent(redemptionType));

if (disableBackground) {
settingsParam.append('disableBackground', 'true');
Expand Down
Loading
Loading