🎨 Added Offers to the new Settings (#19493)
no issue
- Removes flags for the new Offers in Admin X (Settings)
- Removes old Offers from the sidebar.
- See a new version of Offers in Settings. 🎨
This commit is contained in:
parent
14bf2df834
commit
c7d7b883cc
@ -27,9 +27,11 @@ export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'label'
|
||||
loadingIndicatorColor?: LoadingIndicatorColor;
|
||||
outlineOnMobile?: boolean;
|
||||
onClick?: (e?:React.MouseEvent<HTMLElement>) => void;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
testId,
|
||||
size = 'md',
|
||||
label = '',
|
||||
hideLabel = false,
|
||||
@ -142,6 +144,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
</>;
|
||||
|
||||
const buttonElement = React.createElement(tag, {className: className,
|
||||
'data-testid': testId,
|
||||
disabled: disabled,
|
||||
type: 'button',
|
||||
onClick: onClick,
|
||||
|
@ -416,7 +416,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
(<header className={headerClasses}>
|
||||
{title && <Heading level={3}>{title}</Heading>}
|
||||
<div className={`${topRightContent !== 'close' && 'md:!invisible md:!hidden'} ${hideXOnMobile && 'hidden'} absolute right-6 top-6`}>
|
||||
<Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' iconColorClass='text-black dark:text-white' size='sm' unstyled onClick={removeModal} />
|
||||
<Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' iconColorClass='text-black dark:text-white' size='sm' testId='close-modal' unstyled onClick={removeModal} />
|
||||
</div>
|
||||
</header>)
|
||||
:
|
||||
|
@ -72,8 +72,7 @@ const defaultLabFlags = {
|
||||
outboundLinkTagging: false,
|
||||
announcementBar: false,
|
||||
signupForm: false,
|
||||
members: false,
|
||||
adminXOffers: false
|
||||
members: false
|
||||
};
|
||||
|
||||
// Inject defaultLabFlags into responseFixtures.settings and config
|
||||
|
@ -35,7 +35,7 @@ const Sidebar: React.FC = () => {
|
||||
const {updateRoute} = useRouting();
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const {isAnyTextFieldFocused} = useFocusContext();
|
||||
const hasOffersLabs = useFeatureFlag('adminXOffers');
|
||||
// const hasOffersLabs = useFeatureFlag('adminXOffers');
|
||||
|
||||
// Focus in on search field when pressing "/"
|
||||
useEffect(() => {
|
||||
@ -142,7 +142,7 @@ const Sidebar: React.FC = () => {
|
||||
<SettingNavSection isVisible={checkVisible(Object.values(growthSearchKeywords).flat())} title="Growth">
|
||||
{hasRecommendations && <NavItem icon='heart' keywords={growthSearchKeywords.recommendations} navid='recommendations' title="Recommendations" onClick={handleSectionClick} />}
|
||||
<NavItem icon='emailfield' keywords={growthSearchKeywords.embedSignupForm} navid='embed-signup-form' title="Embeddable signup form" onClick={handleSectionClick} />
|
||||
{hasOffersLabs && hasStripeEnabled && <NavItem icon='discount' keywords={growthSearchKeywords.offers} navid='offers' title="Offers" onClick={handleSectionClick} />}
|
||||
{hasStripeEnabled && <NavItem icon='discount' keywords={growthSearchKeywords.offers} navid='offers' title="Offers" onClick={handleSectionClick} />}
|
||||
{hasTipsAndDonations && <NavItem icon='piggybank' keywords={growthSearchKeywords.tips} navid='tips-or-donations' title="Tips or donations" onClick={handleSectionClick} />}
|
||||
</SettingNavSection>
|
||||
|
||||
|
@ -47,10 +47,6 @@ const features = [{
|
||||
title: 'Tips & donations',
|
||||
description: 'Enables publishers to collect one-time payments',
|
||||
flag: 'tipsAndDonations'
|
||||
},{
|
||||
title: 'AdminX Offers',
|
||||
description: 'Enables the new offers UI in AdminX settings',
|
||||
flag: 'adminXOffers'
|
||||
},{
|
||||
title: 'Filter by email disabled',
|
||||
description: 'Allows filtering members by email disabled',
|
||||
|
@ -18,7 +18,7 @@ export const searchKeywords = {
|
||||
const GrowthSettings: React.FC = () => {
|
||||
const hasTipsAndDonations = useFeatureFlag('tipsAndDonations');
|
||||
const hasRecommendations = useFeatureFlag('recommendations');
|
||||
const hasOffersLabs = useFeatureFlag('adminXOffers');
|
||||
// const hasOffersLabs = useFeatureFlag('adminXOffers');
|
||||
const {config, settings} = useGlobalData();
|
||||
const hasStripeEnabled = checkStripeEnabled(settings || [], config || {});
|
||||
|
||||
@ -26,7 +26,7 @@ const GrowthSettings: React.FC = () => {
|
||||
<SearchableSection keywords={Object.values(searchKeywords).flat()} title='Growth'>
|
||||
{hasRecommendations && <Recommendations keywords={searchKeywords.recommendations} />}
|
||||
<EmbedSignupForm keywords={searchKeywords.embedSignupForm} />
|
||||
{hasOffersLabs && hasStripeEnabled && <Offers keywords={searchKeywords.offers} />}
|
||||
{hasStripeEnabled && <Offers keywords={searchKeywords.offers} />}
|
||||
{hasTipsAndDonations && <TipsOrDonations keywords={searchKeywords.tips} />}
|
||||
</SearchableSection>
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import PortalFrame from '../../membership/portal/PortalFrame';
|
||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||
import {Button} from '@tryghost/admin-x-design-system';
|
||||
import {ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks';
|
||||
import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system';
|
||||
@ -11,7 +10,6 @@ import {useAddOffer} from '@tryghost/admin-x-framework/api/offers';
|
||||
import {useBrowseOffers} from '@tryghost/admin-x-framework/api/offers';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
import {useModal} from '@ebay/nice-modal-react';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
// we should replace this with a library
|
||||
@ -211,6 +209,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
|
||||
<Select
|
||||
options={tierOptions}
|
||||
selectedOption={selectedTier}
|
||||
testId='tier-cadence-select-offers'
|
||||
title='Tier — Cadence'
|
||||
onSelect={(e) => {
|
||||
if (e) {
|
||||
@ -241,6 +240,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
|
||||
controlClasses={{menu: 'w-20 right-0'}}
|
||||
options={amountOptions}
|
||||
selectedOption={overrides.type === 'percent' ? amountOptions[0] : amountOptions[1]}
|
||||
testId='amount-type-select-offers'
|
||||
onSelect={(e) => {
|
||||
handleAmountTypeChange(e?.value || '');
|
||||
}}
|
||||
@ -250,6 +250,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
|
||||
<Select
|
||||
options={filteredDurationOptions}
|
||||
selectedOption={filteredDurationOptions.find(option => option.value === overrides.duration)}
|
||||
testId='duration-select-offers'
|
||||
title='Duration'
|
||||
onSelect={e => handleDurationChange(e?.value || '')}
|
||||
/>
|
||||
@ -316,9 +317,7 @@ const AddOfferModal = () => {
|
||||
];
|
||||
|
||||
const [href, setHref] = useState<string>('');
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
const hasOffers = useFeatureFlag('adminXOffers');
|
||||
const {data: {tiers} = {}} = useBrowseTiers();
|
||||
const activeTiers = getPaidActiveTiers(tiers || []);
|
||||
const tierCadenceOptions = getTiersCadences(activeTiers);
|
||||
@ -556,13 +555,6 @@ const AddOfferModal = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasOffers) {
|
||||
modal.remove();
|
||||
updateRoute('');
|
||||
}
|
||||
}, [hasOffers, modal, updateRoute]);
|
||||
|
||||
const cancelAddOffer = () => {
|
||||
if (allOffers.length > 0) {
|
||||
updateRoute('offers/edit');
|
||||
|
@ -1,6 +1,6 @@
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import PortalFrame from '../../membership/portal/PortalFrame';
|
||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||
// import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||
import {Button, ConfirmationModal, Form, PreviewModalContent, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
|
||||
import {Offer, useBrowseOffersById, useEditOffer} from '@tryghost/admin-x-framework/api/offers';
|
||||
@ -177,21 +177,12 @@ const Sidebar: React.FC<{
|
||||
|
||||
const EditOfferModal: React.FC<{id: string}> = ({id}) => {
|
||||
const {siteData} = useGlobalData();
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
const handleError = useHandleError();
|
||||
const hasOffers = useFeatureFlag('adminXOffers');
|
||||
const {mutateAsync: editOffer} = useEditOffer();
|
||||
|
||||
const [href, setHref] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasOffers) {
|
||||
modal.remove();
|
||||
updateRoute('');
|
||||
}
|
||||
}, [hasOffers, modal, updateRoute]);
|
||||
|
||||
const {data: {offers: offerById = []} = {}} = useBrowseOffersById(id ? id : '');
|
||||
|
||||
const {formState, saveState, updateForm, setFormState, handleSave, validate, errors, clearError, okProps} = useForm({
|
||||
|
@ -103,7 +103,7 @@ const OfferSuccess: React.FC<{id: string}> = ({id}) => {
|
||||
<p className='mt-3 max-w-[510px] text-[1.6rem]'>You can share the link anywhere. In your newsletter, social media, a podcast, or in-person. It all just works.</p>
|
||||
<div className='mt-8 flex w-full max-w-md flex-col gap-8'>
|
||||
<div className='flex flex-col-reverse gap-2'>
|
||||
<TextField type='url' value={offerLink} disabled />
|
||||
<TextField name='offer-url' type='url' value={offerLink} disabled />
|
||||
<Button color='green' label={isCopied ? 'Copied!' : 'Copy link'} fullWidth onClick={handleCopyClick} />
|
||||
</div>
|
||||
<div className='flex items-center gap-4 text-xs font-medium before:h-px before:grow before:bg-grey-300 before:content-[""] after:h-px after:grow after:bg-grey-300 after:content-[""] dark:before:bg-grey-800 dark:after:bg-grey-800'>OR</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||
import {Button, Tab, TabView} from '@tryghost/admin-x-design-system';
|
||||
import {ButtonGroup, ButtonProps} from '@tryghost/admin-x-design-system';
|
||||
import {Modal} from '@tryghost/admin-x-design-system';
|
||||
@ -10,11 +9,11 @@ import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
|
||||
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
|
||||
import {numberWithCommas} from '../../../../utils/helpers';
|
||||
import {useBrowseOffers} from '@tryghost/admin-x-framework/api/offers';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
import {useModal} from '@ebay/nice-modal-react';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
import {useSortingState} from '../../../providers/SettingsAppProvider';
|
||||
import {useState} from 'react';
|
||||
|
||||
export type OfferType = 'percent' | 'fixed' | 'trial';
|
||||
|
||||
@ -61,7 +60,6 @@ export const getOfferDiscount = (type: string, amount: number, cadence: string,
|
||||
break;
|
||||
};
|
||||
|
||||
// Check if updatedPrice is negative, if so, set it to 0
|
||||
if (updatedPrice < 0) {
|
||||
updatedPrice = 0;
|
||||
}
|
||||
@ -94,7 +92,6 @@ export const CopyLinkButton: React.FC<{offerCode: string}> = ({offerCode}) => {
|
||||
export const OffersIndexModal = () => {
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
const hasOffers = useFeatureFlag('adminXOffers');
|
||||
const {data: {offers: allOffers = []} = {}} = useBrowseOffers({
|
||||
searchParams: {
|
||||
limit: 'all'
|
||||
@ -123,13 +120,6 @@ export const OffersIndexModal = () => {
|
||||
const sortOption = offersSorting?.option || 'date-added';
|
||||
const sortDirection = offersSorting?.direction || 'desc';
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasOffers) {
|
||||
modal.remove();
|
||||
updateRoute('');
|
||||
}
|
||||
}, [hasOffers, modal, updateRoute]);
|
||||
|
||||
const handleOfferEdit = (id:string) => {
|
||||
// TODO: implement
|
||||
sessionStorage.setItem('editOfferPageSource', 'offersIndex');
|
||||
@ -145,7 +135,6 @@ export const OffersIndexModal = () => {
|
||||
case 'redemptions':
|
||||
return multiplier * (offer1.redemption_count - offer2.redemption_count);
|
||||
default:
|
||||
// 'date-added' or unknown option, use default sorting
|
||||
return multiplier * ((offer1.created_at ? new Date(offer1.created_at).getTime() : 0) - (offer2.created_at ? new Date(offer2.created_at).getTime() : 0));
|
||||
}
|
||||
});
|
||||
@ -164,7 +153,6 @@ export const OffersIndexModal = () => {
|
||||
}
|
||||
{sortedOffers.filter((offer) => {
|
||||
const offerTier = allTiers?.find(tier => tier.id === offer?.tier.id);
|
||||
//Check to filter out offers with archived offerTier
|
||||
return (selectedTab === 'active' && (offer.status === 'active' && offerTier && offerTier.active === true)) ||
|
||||
(selectedTab === 'archived' && (offer.status === 'archived' || (offerTier && offerTier.active === false)));
|
||||
}).map((offer) => {
|
||||
@ -179,7 +167,7 @@ export const OffersIndexModal = () => {
|
||||
const {discountOffer, originalPriceWithCurrency, updatedPriceWithCurrency} = getOfferDiscount(offer.type, offer.amount, offer.cadence, offer.currency || 'USD', offerTier);
|
||||
|
||||
return (
|
||||
<tr className={`group relative scale-100 border-b border-b-grey-200 dark:border-grey-800`}>
|
||||
<tr className={`group relative scale-100 border-b border-b-grey-200 dark:border-grey-800`} data-testid="offer-item">
|
||||
<td className={`${isTierArchived ? 'opacity-50' : ''} p-0`}><a className={`block ${isTierArchived ? 'cursor-default select-none' : 'cursor-pointer'} p-5 pl-0`} onClick={!isTierArchived ? () => handleOfferEdit(offer?.id ? offer.id : '') : () => {}}><span className='font-semibold'>{offer?.name}</span><br /><span className='text-sm text-grey-700'>{offerTier.name} {getOfferCadence(offer.cadence)}</span></a></td>
|
||||
<td className={`${isTierArchived ? 'opacity-50' : ''} whitespace-nowrap p-0 text-sm`}><a className={`block ${isTierArchived ? 'cursor-default select-none' : 'cursor-pointer'} p-5`} onClick={!isTierArchived ? () => handleOfferEdit(offer?.id ? offer.id : '') : () => {}}><span className='text-[1.3rem] font-medium uppercase'>{discountOffer}</span><br /><span className='text-grey-700'>{offer.type !== 'trial' ? getOfferDuration(offer.duration) : 'Trial period'}</span></a></td>
|
||||
<td className={`${isTierArchived ? 'opacity-50' : ''} whitespace-nowrap p-0 text-sm`}><a className={`block ${isTierArchived ? 'cursor-default select-none' : 'cursor-pointer'} p-5`} onClick={!isTierArchived ? () => handleOfferEdit(offer?.id ? offer.id : '') : () => {}}><span className='font-medium'>{updatedPriceWithCurrency}</span> {offer.type !== 'trial' ? <span className='relative text-xs text-grey-700 before:absolute before:-inset-x-0.5 before:top-1/2 before:rotate-[-20deg] before:border-t before:content-[""]'>{originalPriceWithCurrency}</span> : null}</a></td>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests} from '../../utils/acceptance';
|
||||
import {mockApi, responseFixtures, settingsWithStripe, toggleLabsFlag} from '@tryghost/admin-x-framework/test/acceptance';
|
||||
import {mockApi, responseFixtures, settingsWithStripe} from '@tryghost/admin-x-framework/test/acceptance';
|
||||
|
||||
test.describe('Offers Modal', () => {
|
||||
test.beforeEach(async () => {
|
||||
toggleLabsFlag('adminXOffers', true);
|
||||
});
|
||||
// test.beforeEach(async () => {
|
||||
// toggleLabsFlag('adminXOffers', true);
|
||||
// });
|
||||
|
||||
test('Offers Modal is available', async ({page}) => {
|
||||
await mockApi({page, requests: {
|
||||
|
@ -54,8 +54,6 @@ test.describe('Tier settings', async () => {
|
||||
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await page.pause();
|
||||
|
||||
// await expect(section.getByTestId('tier-card').filter({hasText: /Plus/})).toHaveText(/Plus tier/);
|
||||
// await expect(section.getByTestId('tier-card').filter({hasText: /Plus/})).toHaveText(/\$8\/month/);
|
||||
|
||||
|
@ -90,9 +90,6 @@ test.describe('Actions', async () => {
|
||||
await replyButton.click();
|
||||
const editor = frame.getByTestId('form-editor');
|
||||
await expect(editor).toBeVisible();
|
||||
|
||||
await page.pause();
|
||||
|
||||
// Wait for focused
|
||||
await waitEditorFocused(editor);
|
||||
|
||||
|
@ -351,3 +351,4 @@ add|ember-template-lint|no-passed-in-event-handlers|50|48|50|48|138f75fcc7408e61
|
||||
add|ember-template-lint|no-passed-in-event-handlers|76|48|76|48|8fb95409c6333d15490f0777239f6e04641215e6|1697068800000|1707440400000|1712620800000|app/templates/settings/integrations/slack.hbs
|
||||
add|ember-template-lint|require-valid-alt-text|24|20|24|20|a500be6114ce681e610b6fec7d2a6cea2f4555f7|1697068800000|1707440400000|1712620800000|app/templates/settings/integrations/slack.hbs
|
||||
add|ember-template-lint|no-action|51|56|51|56|2b83cff3852a6f3cb1f2d2613e1bb611e1af4996|1697068800000|1707440400000|1712620800000|app/templates/settings/integrations/unsplash.hbs
|
||||
remove|ember-template-lint|no-unknown-arguments-for-builtin-components|118|95|118|95|b8aae2daed1c14cf280800b3d282d11fb14851a4|1697068800000|1707440400000|1712620800000|app/components/gh-nav-menu/main.hbs
|
||||
|
@ -117,11 +117,6 @@
|
||||
{{/if}}
|
||||
</li>
|
||||
|
||||
{{#if this.settings.paidMembersEnabled}}
|
||||
<li>
|
||||
<LinkTo @route="offers" @current-when="offers offer offer.new" @alt="Offers" data-test-nav="offers">{{svg-jar "percentage"}}Offers</LinkTo>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (feature "adminXDemo")}}
|
||||
<li>
|
||||
|
@ -82,10 +82,10 @@ Router.map(function () {
|
||||
this.route('member', {path: '/members/:member_id'});
|
||||
this.route('members-activity');
|
||||
|
||||
this.route('offers');
|
||||
// this.route('offers');
|
||||
|
||||
this.route('offer.new', {path: '/offers/new'});
|
||||
this.route('offer', {path: '/offers/:offer_id'});
|
||||
// this.route('offer.new', {path: '/offers/new'});
|
||||
// this.route('offer', {path: '/offers/:offer_id'});
|
||||
|
||||
this.route('error404', {path: '/*path'});
|
||||
|
||||
|
@ -1,80 +1,80 @@
|
||||
import AdminRoute from 'ghost-admin/routes/admin';
|
||||
import ConfirmUnsavedChangesModal from '../components/modals/confirm-unsaved-changes';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
// import AdminRoute from 'ghost-admin/routes/admin';
|
||||
// import ConfirmUnsavedChangesModal from '../components/modals/confirm-unsaved-changes';
|
||||
// import {action} from '@ember/object';
|
||||
// import {inject as service} from '@ember/service';
|
||||
|
||||
export default class OffersRoute extends AdminRoute {
|
||||
@service modals;
|
||||
@service router;
|
||||
// export default class OffersRoute extends AdminRoute {
|
||||
// @service modals;
|
||||
// @service router;
|
||||
|
||||
_requiresBackgroundRefresh = true;
|
||||
// _requiresBackgroundRefresh = true;
|
||||
|
||||
model(params) {
|
||||
this._requiresBackgroundRefresh = false;
|
||||
// model(params) {
|
||||
// this._requiresBackgroundRefresh = false;
|
||||
|
||||
if (params.offer_id) {
|
||||
return this.store.queryRecord('offer', {id: params.offer_id});
|
||||
} else {
|
||||
return this.store.createRecord('offer');
|
||||
}
|
||||
}
|
||||
// if (params.offer_id) {
|
||||
// return this.store.queryRecord('offer', {id: params.offer_id});
|
||||
// } else {
|
||||
// return this.store.createRecord('offer');
|
||||
// }
|
||||
// }
|
||||
|
||||
setupController(controller, offer) {
|
||||
super.setupController(...arguments);
|
||||
// setupController(controller, offer) {
|
||||
// super.setupController(...arguments);
|
||||
|
||||
if (this._requiresBackgroundRefresh) {
|
||||
controller.fetchOfferTask.perform(offer.id);
|
||||
}
|
||||
}
|
||||
// if (this._requiresBackgroundRefresh) {
|
||||
// controller.fetchOfferTask.perform(offer.id);
|
||||
// }
|
||||
// }
|
||||
|
||||
deactivate() {
|
||||
// clean up newly created records and revert unsaved changes to existing
|
||||
this.controller.offer.rollbackAttributes();
|
||||
this._requiresBackgroundRefresh = true;
|
||||
}
|
||||
// deactivate() {
|
||||
// // clean up newly created records and revert unsaved changes to existing
|
||||
// this.controller.offer.rollbackAttributes();
|
||||
// this._requiresBackgroundRefresh = true;
|
||||
// }
|
||||
|
||||
@action
|
||||
async willTransition(transition) {
|
||||
if (this.hasConfirmed) {
|
||||
return true;
|
||||
}
|
||||
// @action
|
||||
// async willTransition(transition) {
|
||||
// if (this.hasConfirmed) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
transition.abort();
|
||||
// transition.abort();
|
||||
|
||||
// wait for any existing confirm modal to be closed before allowing transition
|
||||
if (this.confirmModal) {
|
||||
return;
|
||||
}
|
||||
// // wait for any existing confirm modal to be closed before allowing transition
|
||||
// if (this.confirmModal) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const shouldLeave = await this.confirmUnsavedChanges();
|
||||
// const shouldLeave = await this.confirmUnsavedChanges();
|
||||
|
||||
if (shouldLeave) {
|
||||
this.controller.model.rollbackAttributes();
|
||||
this.hasConfirmed = true;
|
||||
return transition.retry();
|
||||
}
|
||||
}
|
||||
// if (shouldLeave) {
|
||||
// this.controller.model.rollbackAttributes();
|
||||
// this.hasConfirmed = true;
|
||||
// return transition.retry();
|
||||
// }
|
||||
// }
|
||||
|
||||
async confirmUnsavedChanges() {
|
||||
if (this.controller.model?.hasDirtyAttributes) {
|
||||
this.confirmModal = this.modals
|
||||
.open(ConfirmUnsavedChangesModal)
|
||||
.finally(() => {
|
||||
this.confirmModal = null;
|
||||
});
|
||||
// async confirmUnsavedChanges() {
|
||||
// if (this.controller.model?.hasDirtyAttributes) {
|
||||
// this.confirmModal = this.modals
|
||||
// .open(ConfirmUnsavedChangesModal)
|
||||
// .finally(() => {
|
||||
// this.confirmModal = null;
|
||||
// });
|
||||
|
||||
return this.confirmModal;
|
||||
}
|
||||
// return this.confirmModal;
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
// return true;
|
||||
// }
|
||||
|
||||
@action
|
||||
save() {
|
||||
this.controller.save();
|
||||
}
|
||||
// @action
|
||||
// save() {
|
||||
// this.controller.save();
|
||||
// }
|
||||
|
||||
titleToken() {
|
||||
return this.controller.offer.name;
|
||||
}
|
||||
}
|
||||
// titleToken() {
|
||||
// return this.controller.offer.name;
|
||||
// }
|
||||
// }
|
||||
|
@ -1,18 +1,18 @@
|
||||
import OfferRoute from '../offer';
|
||||
import {inject as service} from '@ember/service';
|
||||
// import OfferRoute from '../offer';
|
||||
// import {inject as service} from '@ember/service';
|
||||
|
||||
export default class NewOfferRoute extends OfferRoute {
|
||||
@service membersUtils;
|
||||
// export default class NewOfferRoute extends OfferRoute {
|
||||
// @service membersUtils;
|
||||
|
||||
controllerName = 'offer';
|
||||
templateName = 'offer';
|
||||
// controllerName = 'offer';
|
||||
// templateName = 'offer';
|
||||
|
||||
/**
|
||||
* First check if we have active tiers
|
||||
*/
|
||||
beforeModel() {
|
||||
if (!this.membersUtils.hasActiveTiers) {
|
||||
return this.replaceWith('offers');
|
||||
}
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * First check if we have active tiers
|
||||
// */
|
||||
// beforeModel() {
|
||||
// if (!this.membersUtils.hasActiveTiers) {
|
||||
// return this.replaceWith('offers');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
@ -1,31 +1,31 @@
|
||||
import AdminRoute from 'ghost-admin/routes/admin';
|
||||
import {inject as service} from '@ember/service';
|
||||
// import AdminRoute from 'ghost-admin/routes/admin';
|
||||
// import {inject as service} from '@ember/service';
|
||||
|
||||
export default class offersRoute extends AdminRoute {
|
||||
@service store;
|
||||
@service feature;
|
||||
// export default class offersRoute extends AdminRoute {
|
||||
// @service store;
|
||||
// @service feature;
|
||||
|
||||
queryParams = {
|
||||
type: {refreshModel: true}
|
||||
};
|
||||
// queryParams = {
|
||||
// type: {refreshModel: true}
|
||||
// };
|
||||
|
||||
beforeModel() {
|
||||
super.beforeModel(...arguments);
|
||||
// TODO: redirect if members is disabled?
|
||||
}
|
||||
// beforeModel() {
|
||||
// super.beforeModel(...arguments);
|
||||
// // TODO: redirect if members is disabled?
|
||||
// }
|
||||
|
||||
model(params) {
|
||||
return this.controllerFor('offers').fetchOffersTask.perform(params);
|
||||
}
|
||||
// model(params) {
|
||||
// return this.controllerFor('offers').fetchOffersTask.perform(params);
|
||||
// }
|
||||
|
||||
// trigger a background load of members plus labels for filter dropdown
|
||||
setupController() {
|
||||
super.setupController(...arguments);
|
||||
}
|
||||
// // trigger a background load of members plus labels for filter dropdown
|
||||
// setupController() {
|
||||
// super.setupController(...arguments);
|
||||
// }
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
titleToken: 'Offers'
|
||||
};
|
||||
}
|
||||
}
|
||||
// buildRouteInfoMetadata() {
|
||||
// return {
|
||||
// titleToken: 'Offers'
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
@ -1,111 +1,111 @@
|
||||
import moment from 'moment-timezone';
|
||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||
import {beforeEach, describe, it} from 'mocha';
|
||||
import {blur, click, currentURL, fillIn, find, findAll, settled} from '@ember/test-helpers';
|
||||
import {enablePaidMembers} from '../helpers/members';
|
||||
import {expect} from 'chai';
|
||||
import {setupApplicationTest} from 'ember-mocha';
|
||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
import {timeout} from 'ember-concurrency';
|
||||
import {visit} from '../helpers/visit';
|
||||
// import moment from 'moment-timezone';
|
||||
// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||
// import {beforeEach, describe, it} from 'mocha';
|
||||
// import {blur, click, currentURL, fillIn, find, findAll, settled} from '@ember/test-helpers';
|
||||
// import {enablePaidMembers} from '../helpers/members';
|
||||
// import {expect} from 'chai';
|
||||
// import {setupApplicationTest} from 'ember-mocha';
|
||||
// import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
// import {timeout} from 'ember-concurrency';
|
||||
// import {visit} from '../helpers/visit';
|
||||
|
||||
describe('Acceptance: Offers', function () {
|
||||
let hooks = setupApplicationTest();
|
||||
setupMirage(hooks);
|
||||
// describe('Acceptance: Offers', function () {
|
||||
// let hooks = setupApplicationTest();
|
||||
// setupMirage(hooks);
|
||||
|
||||
it('redirects to signin when not authenticated', async function () {
|
||||
await invalidateSession();
|
||||
await visit('/offers');
|
||||
// it('redirects to signin when not authenticated', async function () {
|
||||
// await invalidateSession();
|
||||
// await visit('/offers');
|
||||
|
||||
expect(currentURL()).to.equal('/signin');
|
||||
});
|
||||
// expect(currentURL()).to.equal('/signin');
|
||||
// });
|
||||
|
||||
it('redirects non-admins to site', async function () {
|
||||
let role = this.server.create('role', {name: 'Editor'});
|
||||
this.server.create('user', {roles: [role]});
|
||||
// it('redirects non-admins to site', async function () {
|
||||
// let role = this.server.create('role', {name: 'Editor'});
|
||||
// this.server.create('user', {roles: [role]});
|
||||
|
||||
await authenticateSession();
|
||||
await visit('/offers');
|
||||
// await authenticateSession();
|
||||
// await visit('/offers');
|
||||
|
||||
expect(currentURL()).to.equal('/site');
|
||||
expect(find('[data-test-nav="offers"]'), 'sidebar link')
|
||||
.to.not.exist;
|
||||
});
|
||||
// expect(currentURL()).to.equal('/site');
|
||||
// expect(find('[data-test-nav="offers"]'), 'sidebar link')
|
||||
// .to.not.exist;
|
||||
// });
|
||||
|
||||
describe('as owner', function () {
|
||||
beforeEach(async function () {
|
||||
this.server.loadFixtures('tiers');
|
||||
// describe('as owner', function () {
|
||||
// beforeEach(async function () {
|
||||
// this.server.loadFixtures('tiers');
|
||||
|
||||
let role = this.server.create('role', {name: 'Owner'});
|
||||
this.server.create('user', {roles: [role]});
|
||||
// let role = this.server.create('role', {name: 'Owner'});
|
||||
// this.server.create('user', {roles: [role]});
|
||||
|
||||
enablePaidMembers(this.server);
|
||||
// enablePaidMembers(this.server);
|
||||
|
||||
return await authenticateSession();
|
||||
});
|
||||
// return await authenticateSession();
|
||||
// });
|
||||
|
||||
it('it renders, can be navigated, can edit offer', async function () {
|
||||
const tier = this.server.create('tier');
|
||||
let offer1 = this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(1, 'day').valueOf()});
|
||||
this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(2, 'day').valueOf()});
|
||||
// it('it renders, can be navigated, can edit offer', async function () {
|
||||
// const tier = this.server.create('tier');
|
||||
// let offer1 = this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(1, 'day').valueOf()});
|
||||
// this.server.create('offer', {tier: {id: tier.id}, createdAt: moment.utc().subtract(2, 'day').valueOf()});
|
||||
|
||||
await visit('/offers');
|
||||
// await visit('/offers');
|
||||
|
||||
await settled();
|
||||
// await settled();
|
||||
|
||||
// lands on correct page
|
||||
expect(currentURL(), 'currentURL').to.equal('/offers');
|
||||
// // lands on correct page
|
||||
// expect(currentURL(), 'currentURL').to.equal('/offers');
|
||||
|
||||
// it highlights active state in nav menu
|
||||
expect(
|
||||
find('[data-test-nav="offers"]'),
|
||||
'highlights nav menu item'
|
||||
).to.have.class('active');
|
||||
// // it highlights active state in nav menu
|
||||
// expect(
|
||||
// find('[data-test-nav="offers"]'),
|
||||
// 'highlights nav menu item'
|
||||
// ).to.have.class('active');
|
||||
|
||||
// it lists all offers
|
||||
expect(findAll('[data-test-list="offers-list-item"]').length, 'offers list count')
|
||||
.to.equal(2);
|
||||
// // it lists all offers
|
||||
// expect(findAll('[data-test-list="offers-list-item"]').length, 'offers list count')
|
||||
// .to.equal(2);
|
||||
|
||||
let offer = find('[data-test-list="offers-list-item"]');
|
||||
expect(offer.querySelector('[data-test-list="offer-name"] h3').textContent, 'offer list item name')
|
||||
.to.equal(offer1.name);
|
||||
// let offer = find('[data-test-list="offers-list-item"]');
|
||||
// expect(offer.querySelector('[data-test-list="offer-name"] h3').textContent, 'offer list item name')
|
||||
// .to.equal(offer1.name);
|
||||
|
||||
await visit(`/offers/${offer1.id}`);
|
||||
// await visit(`/offers/${offer1.id}`);
|
||||
|
||||
// second wait is needed for the offer details to settle
|
||||
await settled();
|
||||
// // second wait is needed for the offer details to settle
|
||||
// await settled();
|
||||
|
||||
// it shows selected offer form
|
||||
expect(find('[data-test-input="offer-name"]').value, 'loads correct offer into form')
|
||||
.to.equal(offer1.name);
|
||||
// // it shows selected offer form
|
||||
// expect(find('[data-test-input="offer-name"]').value, 'loads correct offer into form')
|
||||
// .to.equal(offer1.name);
|
||||
|
||||
// it maintains active state in nav menu
|
||||
expect(
|
||||
find('[data-test-nav="offers"]'),
|
||||
'highlights nav menu item'
|
||||
).to.have.class('active');
|
||||
// // it maintains active state in nav menu
|
||||
// expect(
|
||||
// find('[data-test-nav="offers"]'),
|
||||
// 'highlights nav menu item'
|
||||
// ).to.have.class('active');
|
||||
|
||||
// trigger save
|
||||
await fillIn('[data-test-input="offer-name"]', 'New Name');
|
||||
await blur('[data-test-input="offer-name"]');
|
||||
// // trigger save
|
||||
// await fillIn('[data-test-input="offer-name"]', 'New Name');
|
||||
// await blur('[data-test-input="offer-name"]');
|
||||
|
||||
await click('[data-test-button="save"]');
|
||||
// await click('[data-test-button="save"]');
|
||||
|
||||
// extra timeout needed for Travis - sometimes it doesn't update
|
||||
// quick enough and an extra wait() call doesn't help
|
||||
await timeout(100);
|
||||
// // extra timeout needed for Travis - sometimes it doesn't update
|
||||
// // quick enough and an extra wait() call doesn't help
|
||||
// await timeout(100);
|
||||
|
||||
await click('[data-test-link="offers-back"]');
|
||||
// await click('[data-test-link="offers-back"]');
|
||||
|
||||
// lands on correct page
|
||||
expect(currentURL(), 'currentURL').to.equal('/offers');
|
||||
});
|
||||
// // lands on correct page
|
||||
// expect(currentURL(), 'currentURL').to.equal('/offers');
|
||||
// });
|
||||
|
||||
it('maintains active state in nav menu when creating a new tag', async function () {
|
||||
await visit('offers/new');
|
||||
expect(currentURL()).to.equal('offers/new');
|
||||
expect(find('[data-test-nav="offers"]'), 'highlights nav menu item')
|
||||
.to.have.class('active');
|
||||
});
|
||||
});
|
||||
});
|
||||
// it('maintains active state in nav menu when creating a new tag', async function () {
|
||||
// await visit('offers/new');
|
||||
// expect(currentURL()).to.equal('offers/new');
|
||||
// expect(find('[data-test-nav="offers"]'), 'highlights nav menu item')
|
||||
// .to.have.class('active');
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
@ -44,7 +44,7 @@ const ALPHA_FEATURES = [
|
||||
'tipsAndDonations',
|
||||
'importMemberTier',
|
||||
'lexicalIndicators',
|
||||
'adminXOffers',
|
||||
// 'adminXOffers',
|
||||
'filterEmailDisabled',
|
||||
'adminXDemo',
|
||||
'newEmailAddresses',
|
||||
|
@ -23,19 +23,16 @@ test.describe('Admin', () => {
|
||||
monthlyPrice: 5,
|
||||
yearlyPrice: 50
|
||||
});
|
||||
const offerName = await createOffer(sharedPage, {
|
||||
const {offerName} = await createOffer(sharedPage, {
|
||||
name: 'Get 5% Off!',
|
||||
tierName,
|
||||
offerType: 'discount',
|
||||
amount: 5
|
||||
offerType: 'freeTrial',
|
||||
amount: 14
|
||||
});
|
||||
|
||||
await test.step('Check that offers and tiers are available on Offers page', async () => {
|
||||
await sharedPage.locator('[data-test-nav="offers"]').click();
|
||||
await sharedPage.waitForSelector('[data-test-offers-list]');
|
||||
await expect(sharedPage.locator('[data-test-offers-list]')).toContainText(tierName);
|
||||
await expect(sharedPage.locator('[data-test-offers-list]')).toContainText(offerName);
|
||||
});
|
||||
await sharedPage.goto('/ghost/');
|
||||
await sharedPage.goto('/ghost/#/settings/offers');
|
||||
await expect(sharedPage.getByTestId('offers')).toContainText(offerName);
|
||||
});
|
||||
|
||||
test('Can create additional Tier', async ({sharedPage}) => {
|
||||
|
@ -18,7 +18,7 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// add a new offer with free trial
|
||||
const offerName = await createOffer(sharedPage, {
|
||||
const {offerName, offerLink} = await createOffer(sharedPage, {
|
||||
name: 'Black Friday Special',
|
||||
tierName,
|
||||
offerType: 'freeTrial',
|
||||
@ -26,14 +26,10 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// check that offer was added in the offer list screen
|
||||
await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible();
|
||||
await sharedPage.goto('/ghost/#/settings/offers');
|
||||
await expect(sharedPage.getByTestId('offers')).toContainText(offerName);
|
||||
|
||||
// open offer details page
|
||||
await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click();
|
||||
|
||||
// fetch offer url from portal settings and open it
|
||||
const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue();
|
||||
await sharedPage.goto(portalUrl);
|
||||
await sharedPage.goto(offerLink);
|
||||
|
||||
const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]');
|
||||
const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]');
|
||||
@ -66,14 +62,15 @@ test.describe('Portal', () => {
|
||||
await sharedPage.goto('/ghost');
|
||||
await sharedPage.locator('.gh-nav a[href="#/members/"]').click();
|
||||
|
||||
// 1 member, should be Testy, on Portal Tier
|
||||
// // 1 member, should be Testy, on Portal Tier
|
||||
await expect(sharedPage.getByRole('link', {name: 'Testy McTesterson testy+trial@example.com'}), 'Should have 1 paid member').toBeVisible();
|
||||
await expect(sharedPage.getByRole('link', {name: tierName}), `Paid member should be on ${tierName}`).toBeVisible();
|
||||
|
||||
// Ensure the offer redemption count was bumped
|
||||
await sharedPage.locator('.gh-nav a[href="#/offers/"]').click();
|
||||
const locator = sharedPage.locator(`[data-test-offer="${offerName}"]`).locator('[data-test-list="redemption-count"]').locator('span');
|
||||
await expect(locator).toContainText('1');
|
||||
// // Ensure the offer redemption count was bumped
|
||||
await sharedPage.goto('/ghost/#/settings/offers');
|
||||
// await sharedPage.locator('.gh-nav a[href="#/offers/"]').click();
|
||||
const locator = sharedPage.locator(`[data-test-offer="${offerName}"]`);
|
||||
await expect(locator).toContainText('1 redemption');
|
||||
});
|
||||
|
||||
test('Creates and uses a one-time discount Offer', async ({sharedPage}) => {
|
||||
@ -90,7 +87,7 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// Creates a one-time discount offer for 10% off
|
||||
const offerName = await createOffer(sharedPage, {
|
||||
const {offerName, offerLink} = await createOffer(sharedPage, {
|
||||
name: 'Black Friday Special',
|
||||
tierName: tierName,
|
||||
offerType: 'discount',
|
||||
@ -98,14 +95,13 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// check that offer was added in the offer list screen
|
||||
await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible();
|
||||
|
||||
await sharedPage.goto('/ghost/#/settings/offers');
|
||||
await expect(sharedPage.getByTestId('offers')).toContainText(offerName);
|
||||
// open offer details page
|
||||
await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click();
|
||||
// await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click();
|
||||
|
||||
// fetch offer url from portal settings and open it
|
||||
const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue();
|
||||
await sharedPage.goto(portalUrl);
|
||||
await sharedPage.goto(offerLink);
|
||||
|
||||
const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]');
|
||||
const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]');
|
||||
@ -155,7 +151,7 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// Creates a one-time discount offer for 10% off
|
||||
const offerName = await createOffer(sharedPage, {
|
||||
const {offerName, offerLink} = await createOffer(sharedPage, {
|
||||
name: 'Black Friday Special',
|
||||
tierName: tierName,
|
||||
offerType: 'discount',
|
||||
@ -165,14 +161,10 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// check that offer was added in the offer list screen
|
||||
await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible();
|
||||
await sharedPage.goto('/ghost/#/settings/offers');
|
||||
await expect(sharedPage.getByTestId('offers')).toContainText(offerName);
|
||||
|
||||
// open offer details page
|
||||
await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click();
|
||||
|
||||
// fetch offer url from portal settings and open it
|
||||
const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue();
|
||||
await sharedPage.goto(portalUrl);
|
||||
await sharedPage.goto(offerLink);
|
||||
|
||||
const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]');
|
||||
const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]');
|
||||
@ -223,7 +215,7 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// Creates a one-time discount offer for 10% off
|
||||
const offerName = await createOffer(sharedPage, {
|
||||
const {offerName, offerLink} = await createOffer(sharedPage, {
|
||||
name: 'Black Friday Special',
|
||||
tierName: tierName,
|
||||
offerType: 'discount',
|
||||
@ -232,14 +224,10 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// check that offer was added in the offer list screen
|
||||
await expect(sharedPage.locator(`[data-test-offer="${offerName}"]`), 'Should have free-trial offer').toBeVisible();
|
||||
await sharedPage.goto('/ghost/#/settings/offers');
|
||||
await expect(sharedPage.getByTestId('offers')).toContainText(offerName);
|
||||
|
||||
// open offer details page
|
||||
await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click();
|
||||
|
||||
// fetch offer url from portal settings and open it
|
||||
const portalUrl = await sharedPage.locator('[data-test-input="offer-portal-url"]').inputValue();
|
||||
await sharedPage.goto(portalUrl);
|
||||
await sharedPage.goto(offerLink);
|
||||
|
||||
const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]');
|
||||
const portalFrame = sharedPage.frameLocator('[data-testid="portal-popup-frame"]');
|
||||
@ -286,7 +274,7 @@ test.describe('Portal', () => {
|
||||
});
|
||||
|
||||
// Create an offer. This will be archived
|
||||
const offerName = await createOffer(sharedPage, {
|
||||
const {offerLink} = await createOffer(sharedPage, {
|
||||
name: 'To be archived',
|
||||
tierName: tierName,
|
||||
offerType: 'discount',
|
||||
@ -300,18 +288,8 @@ test.describe('Portal', () => {
|
||||
offerType: 'discount',
|
||||
amount: 10
|
||||
});
|
||||
|
||||
// Check if the offer appears in the archive list
|
||||
await sharedPage.locator('.gh-contentfilter-menu-trigger').click();
|
||||
await sharedPage.getByRole('option', {name: 'Archived offers'}).click();
|
||||
await expect(sharedPage.getByRole('link', {name: offerName}), 'Should have an archived offer').toBeVisible();
|
||||
|
||||
// Go to the offer and grab the offer URL
|
||||
await sharedPage.locator('.gh-offers-list .gh-list-row').filter({hasText: offerName}).click();
|
||||
const portalUrl = await sharedPage.locator('input#url').inputValue();
|
||||
|
||||
// Open the offer URL and make sure portal popup doesn't load
|
||||
await sharedPage.goto(portalUrl);
|
||||
await sharedPage.goto(offerLink);
|
||||
const portalPopup = await sharedPage.locator('[data-testid="portal-popup-frame"]').isVisible();
|
||||
await expect(portalPopup).toBeFalsy();
|
||||
});
|
||||
|
@ -247,72 +247,71 @@ const createTier = async (page, {name, monthlyPrice, yearlyPrice, trialDays}, en
|
||||
* @param {string} [options.discountType]
|
||||
* @param {number} [options.discountDuration]
|
||||
* @param {number} options.amount
|
||||
* @returns {Promise<string>} Unique offer name
|
||||
* @returns {Promise<object>} Unique offer name
|
||||
*/
|
||||
|
||||
const createOffer = async (page, {name, tierName, offerType, amount, discountType = null, discountDuration = 3}) => {
|
||||
await page.goto('/ghost');
|
||||
await page.locator('.gh-nav a[href="#/offers/"]').click();
|
||||
await page.locator('[data-test-nav="settings"]').click();
|
||||
|
||||
// Keep offer names unique & <= 40 characters
|
||||
let offerName = `${name} (${new ObjectID().toHexString().slice(0, 40 - name.length - 3)})`;
|
||||
// Tiers request can take time, so waiting until there is no connections before interacting with them
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const hasExistingOffers = await page.getByTestId('offers').getByRole('button', {name: 'Manage offers'}).isVisible();
|
||||
const isCTA = await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).isVisible();
|
||||
// Archive other offers to keep the list tidy
|
||||
// We only need 1 offer to be active at a time
|
||||
// Either the list of active offers loads, or the CTA when no offers exist
|
||||
while (await Promise.race([
|
||||
page.locator('.gh-offers-list .gh-list-header').filter({hasText: 'active'}).waitFor({state: 'visible', timeout: 1000}).then(() => true),
|
||||
page.locator('.gh-offers-list-cta').waitFor({state: 'visible', timeout: 1000}).then(() => false)
|
||||
]).catch(() => false)) {
|
||||
const listItem = page.locator('.gh-offers-list .gh-list-row:not(.header)').first();
|
||||
await listItem.locator('a[href^="#/offers/"]').last().click();
|
||||
await page.getByRole('button', {name: 'Archive offer'}).click();
|
||||
await page
|
||||
.locator('.modal-content')
|
||||
.filter({hasText: 'Archive offer'})
|
||||
.first()
|
||||
.getByRole('button', {name: 'Archive'})
|
||||
.click();
|
||||
if (hasExistingOffers && !isCTA) {
|
||||
await page.getByTestId('offers').getByRole('button', {name: 'Manage offers'}).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// TODO: Use a more resilient selector
|
||||
const statusDropdown = await page.getByRole('button', {name: 'Archived offers'});
|
||||
await statusDropdown.waitFor({
|
||||
state: 'visible',
|
||||
timeout: 1000
|
||||
});
|
||||
await statusDropdown.click();
|
||||
await page.getByRole('option', {name: 'Active offers'}).click();
|
||||
// Selector for the elements with data-testid 'offer-item'
|
||||
// const offerItemsSelector = '[data-testid="offer-item"]';
|
||||
await page.getByTestId('offer-item').nth(0).click();
|
||||
await page.getByRole('button', {name: 'Archive offer'}).click();
|
||||
|
||||
const confirmModal = page.getByTestId('confirmation-modal');
|
||||
await confirmModal.getByRole('button', {name: 'Archive'}).click();
|
||||
}
|
||||
|
||||
await page.getByRole('link', {name: 'New offer'}).click();
|
||||
await page.locator('input#name').fill(offerName);
|
||||
if (isCTA) {
|
||||
await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).click();
|
||||
} else {
|
||||
await page.getByText('New offer').click();
|
||||
}
|
||||
|
||||
// const newOfferButton = await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}) || await page.getByTestId('offers').getByRole('button', {name: 'New offer'});
|
||||
// await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).click();
|
||||
// await newOfferButton.click();
|
||||
await page.getByLabel('Offer name').fill(offerName);
|
||||
|
||||
if (offerType === 'freeTrial') {
|
||||
await page.getByRole('button', {name: 'Free trial Give free access for a limited time.'}).click();
|
||||
await page.locator('input#trial-duration').fill(`${amount}`);
|
||||
// await page.getByRole('button', {name: 'Free trial Give free access for a limited time.'}).click();
|
||||
await page.getByText('Give free access for a limited time').click();
|
||||
await page.getByLabel('Trial duration').fill(`${amount}`);
|
||||
} else if (offerType === 'discount') {
|
||||
await page.locator('input#amount').fill(`${amount}`);
|
||||
await page.getByLabel('Amount off').fill(`${amount}`);
|
||||
if (discountType === 'multiple-months') {
|
||||
await page.locator('[data-test-select="offer-duration"]').selectOption('repeating');
|
||||
await page.locator('input#duration-months').fill(discountDuration.toString());
|
||||
await chooseOptionInSelect(page.getByTestId('duration-select-offers'), `Multiple-months`);
|
||||
await page.getByLabel('Duration in months').fill(discountDuration.toString());
|
||||
// await page.locator('[data-test-select="offer-duration"]').selectOption('repeating');
|
||||
// await page.locator('input#duration-months').fill(discountDuration.toString());
|
||||
}
|
||||
|
||||
if (discountType === 'forever') {
|
||||
await page.locator('[data-test-select="offer-duration"]').selectOption('forever');
|
||||
await chooseOptionInSelect(page.getByTestId('duration-select-offers'), `Forever`);
|
||||
}
|
||||
}
|
||||
|
||||
const priceId = await page.locator(`.gh-select-product-cadence>select>option`).getByText(`${tierName} - Monthly`).getAttribute('value');
|
||||
await page.locator('.gh-select-product-cadence>select').selectOption(priceId);
|
||||
await chooseOptionInSelect(page.getByTestId('tier-cadence-select-offers'), `${tierName} - Monthly`);
|
||||
await page.getByRole('button', {name: 'Publish'}).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
const offerLink = await page.locator('input[name="offer-url"]').inputValue();
|
||||
|
||||
await page.getByRole('button', {name: 'Save'}).click();
|
||||
// Wait for the "Saved" button, ensures that next clicks don't trigger the unsaved work modal
|
||||
await page.getByRole('button', {name: 'Saved'}).waitFor({
|
||||
state: 'visible',
|
||||
timeout: 10000
|
||||
});
|
||||
await page.locator('.gh-nav a[href="#/offers/"]').click();
|
||||
|
||||
return offerName;
|
||||
return {offerName, offerLink};
|
||||
};
|
||||
|
||||
const fillInputIfExists = async (page, selector, value) => {
|
||||
@ -493,6 +492,11 @@ const generateStripeIntegrationToken = async (accountId) => {
|
||||
})).toString('base64');
|
||||
};
|
||||
|
||||
const chooseOptionInSelect = async (select, optionText) => {
|
||||
await select.click();
|
||||
await select.page().locator('[data-testid="select-option"]', {hasText: optionText}).click();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
setupGhost,
|
||||
setupStripe,
|
||||
@ -509,5 +513,6 @@ module.exports = {
|
||||
completeStripeSubscription,
|
||||
impersonateMember,
|
||||
goToMembershipPage,
|
||||
openTierModal
|
||||
openTierModal,
|
||||
chooseOptionInSelect
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user