Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update recurring discounts #222

Closed
wants to merge 5 commits into from
Closed
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
10 changes: 8 additions & 2 deletions src/components/ActiveDatesCard/ActiveDatesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ export interface ActiveDatesCardProps {
endDate: Field<DateTime | null>;

/**
* The shop's time zone abbreviation. This can be queried from the [Shop gql object](https://shopify.dev/api/admin-graphql/2022-07/objects/Shop#field-shop-timezoneabbreviation).
* The shop's time zone abbreviation. This can be queried from the [Shop gql object](https://shopify.dev/api/admin-graphql/2022-07/objects/Shop#field-timezoneabbreviation).
*/
timezoneAbbreviation: string;

/**
* (optional) The shop's iana time zone. This can be queried from the [Shop gql object](https://shopify.dev/api/admin-graphql/2022-07/objects/Shop#field-ianatimezone).
*/
timezone?: string;

/**
* (optional) The day that should be used as the start of the week.
*
Expand All @@ -48,13 +53,14 @@ export function ActiveDatesCard({
startDate,
endDate,
timezoneAbbreviation,
timezone,
weekStartsOn = DEFAULT_WEEK_START_DAY,
disabled,
}: ActiveDatesCardProps) {
const [i18n] = useI18n();
const nowInUTC = new Date();

const ianaTimezone = i18n.defaultTimezone!;
const ianaTimezone = timezone ?? i18n.defaultTimezone!;
const showEndDate = Boolean(endDate.value);

// When start date or time changes, updates the end date to be later than start date (if applicable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('<ActiveDatesCard />', () => {
startDate: mockField('2023-02-20T18:23:00.000Z'),
endDate: mockField('2023-03-20T18:23:00.000Z'),
locale: 'en-US',
ianaTimeZone: 'America/Toronto',
timezone: 'America/Toronto',
timezoneAbbreviation: 'EDT',
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/MethodCard/MethodCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function MethodCard({
<Box paddingBlockEnd="400">
<Card padding="400">
<BlockStack gap="400">
<InlineStack align="start" blockAlign="center" gap="100">
<InlineStack align="space-between" blockAlign="center">
<Text variant="headingMd" as="h2">
{title}
</Text>
Expand Down
5 changes: 2 additions & 3 deletions src/components/MethodCard/tests/MethodCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,15 @@ describe('<MethodCard />', () => {
});
});

it('renders the title align start and blockAlign center', () => {
it('renders the title align space-betweeen and blockAlign center', () => {
const methodCard = mountWithApp(<MethodCard {...mockProps} />);

expect(methodCard).toContainReactComponent(Card, {
padding: '400',
});
expect(methodCard).toContainReactComponent(InlineStack, {
align: 'start',
align: 'space-between',
blockAlign: 'center',
gap: '100',
});
});

Expand Down
20 changes: 6 additions & 14 deletions src/components/SummaryCard/components/Performance/Performance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import {useI18n} from '@shopify/react-i18n';
import {List, Text, BlockStack, Link} from '@shopify/polaris';

import {DiscountMethod, DiscountStatus} from '../../../../constants';
import {DiscountStatus} from '../../../../constants';
import type {MoneyInput} from '../../../../types';

export interface PerformanceProps {
Expand All @@ -17,32 +17,24 @@ export interface PerformanceProps {
usageCount?: number;

/**
* (optional) Flag that indicates whether a shop has an enabled `ShopFeatures` of `reports` (see https://shopify.dev/api/admin-graphql/2022-04/objects/ShopFeatures#field-shopfeatures-reports)
* (optional) Url to the sales by discount report (this changes based on shop's subscription level)
*/
hasReports?: boolean;

/**
* (optional) When hasReports is true and discountMethod is Code, displays a link to the admin report
*/
discountMethod?: DiscountMethod;
reportsUrl?: string;

/**
* (optional) The total number of sales that have been made with the discount
*/
totalSales?: MoneyInput;
}

const CODE_DISCOUNT_ADMIN_REPORT_URL = `/reports/sales_by_discount`;

const I18N_SCOPE = {
scope: 'DiscountAppComponents.SummaryCard.Performance',
};

export function Performance({
status,
totalSales,
hasReports,
discountMethod,
reportsUrl,
usageCount,
}: PerformanceProps) {
const [i18n] = useI18n();
Expand Down Expand Up @@ -76,9 +68,9 @@ export function Performance({
</List.Item>
)}
</List>
{hasReports && discountMethod === DiscountMethod.Code && (
{reportsUrl && (
<p>
<Link url={CODE_DISCOUNT_ADMIN_REPORT_URL}>
<Link url={reportsUrl} target="_top">
{i18n.translate('performanceLink', I18N_SCOPE)}
</Link>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {CurrencyCode} from '@shopify/react-i18n';
import {Link} from '@shopify/polaris';

import {Performance} from '../Performance';
import {DiscountMethod, DiscountStatus} from '../../../../../constants';
import {DiscountStatus} from '../../../../../constants';

describe('<Performance />', () => {
const mockProps = {
Expand Down Expand Up @@ -53,37 +53,23 @@ describe('<Performance />', () => {
});

describe('Report link', () => {
it('renders a report link when discountMethod is code and the shop has reports', () => {
it('renders a report link when report url is passed in', () => {
const performance = mountWithApp(
<Performance
{...mockProps}
discountMethod={DiscountMethod.Code}
hasReports
reportsUrl="shopify://admin/reports/sales_by_discount"
/>,
);

expect(performance).toContainReactComponent(Link, {
url: '/reports/sales_by_discount',
url: 'shopify://admin/reports/sales_by_discount',
target: '_top',
children: 'View the sales by discount report',
});
});

it('does not render a report link when discountMethod is code and the shop does not have reports', () => {
const performance = mountWithApp(
<Performance {...mockProps} discountMethod={DiscountMethod.Code} />,
);

expect(performance).not.toContainReactComponent(Link);
});

it('does not render a report link when discountMethod is automatic', () => {
const performance = mountWithApp(
<Performance
{...mockProps}
discountMethod={DiscountMethod.Automatic}
hasReports
/>,
);
it('does not render a report link when no report url is passed in', () => {
const performance = mountWithApp(<Performance {...mockProps} />);

expect(performance).not.toContainReactComponent(Link);
});
Expand Down
176 changes: 43 additions & 133 deletions src/components/UsageLimitsCard/UsageLimitsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
import React, {useEffect, useState} from 'react';
import {
Card,
ChoiceList,
TextField,
Text,
InlineError,
BlockStack,
Box,
} from '@shopify/polaris';
import {useI18n} from '@shopify/react-i18n';
import React from 'react';
import {Card, BlockStack, Box} from '@shopify/polaris';

import type {Field, PositiveNumericString} from '../../types';
import type {RecurringPaymentType} from '../../constants';
import {forcePositiveInteger} from '../../utilities/numbers';

import {RecurringPayment} from './components';
import styles from './UsageLimitsCard.scss';

export enum UsageLimitType {
TotalUsageLimit = 'TOTAL_USAGE_LIMIT',
OncePerCustomerLimit = 'ONCE_PER_CUSTOMER_LIMIT',
}
import {RecurringPayment, UsageLimits} from './components';

interface UsageLimitProps {
/**
* The total number of times the discount can be used.
*/
totalUsageLimit: Field<PositiveNumericString | null>;

/**
* When selected, the discount may be used at most once per customer
* (optional) When true, displays the "Recurring payments" section. (see {@interface UsageLimitsCardMultiplePaymentsProps})
*/
oncePerCustomer: Field<boolean>;
isRecurring?: false;

/**
* (optional) When true, displays the "Recurring payments" section. (see {@interface UsageLimitsCardMultiplePaymentsProps})
* (optional) When true, displays the "Limits" section. (see {@interface UsageLimitsCardMultiplePaymentsProps})
*/
isRecurring?: false;
isLimited?: false;
}

interface UsageLimitsCardMultiplePaymentsProps
Expand All @@ -57,118 +36,43 @@ interface UsageLimitsCardMultiplePaymentsProps
recurringPaymentLimit: Field<PositiveNumericString>;
}

interface UsageLimitsCardLimitsProps
extends Omit<UsageLimitProps, 'isLimited'> {
/**
* Displays the "Limits" section.
*/
isLimited: true;

/**
* The total number of times the discount can be used.
*/
totalUsageLimit: Field<PositiveNumericString | null>;

/**
* When selected, the discount may be used at most once per customer
*/
oncePerCustomer: Field<boolean>;
}

export type UsageLimitsCardProps =
| UsageLimitProps
| UsageLimitsCardLimitsProps
| UsageLimitsCardMultiplePaymentsProps;

export const DISCOUNT_TOTAL_USAGE_LIMIT_FIELD = 'totalUsageLimit';

export function UsageLimitsCard(props: UsageLimitsCardProps) {
const {totalUsageLimit, oncePerCustomer, isRecurring} = props;

const [showUsageLimit, setShowUsageLimit] = useState(
totalUsageLimit.value !== null,
);

const [i18n] = useI18n();

useEffect(
() => setShowUsageLimit(totalUsageLimit.value !== null),
[totalUsageLimit.value],
);

const handleUsageLimitsChoicesChange = (
selectedUsageLimitTypes: UsageLimitType[],
) => {
const newOncePerCustomer = selectedUsageLimitTypes.includes(
UsageLimitType.OncePerCustomerLimit,
);

// When the checkbox is toggled, either set the totalUsageLimit value to null (null === checkbox off) or an empty string (non-null === checkbox on)
if (!selectedUsageLimitTypes.includes(UsageLimitType.TotalUsageLimit)) {
totalUsageLimit.onChange(null);
} else if (totalUsageLimit.value === null) {
totalUsageLimit.onChange('');
}

newOncePerCustomer !== oncePerCustomer.value &&
oncePerCustomer.onChange(newOncePerCustomer);
};

if (!props.isLimited && !props.isRecurring) return null;
return (
<Box paddingBlockEnd="400">
<Card padding="400">
<BlockStack gap="400">
<Text variant="headingMd" as="h2">
{i18n.translate('DiscountAppComponents.UsageLimitsCard.title')}
</Text>
<ChoiceList
title={i18n.translate(
'DiscountAppComponents.UsageLimitsCard.options',
)}
titleHidden
allowMultiple
selected={[
...(showUsageLimit ? [UsageLimitType.TotalUsageLimit] : []),
...(oncePerCustomer.value
? [UsageLimitType.OncePerCustomerLimit]
: []),
]}
choices={[
{
label: i18n.translate(
'DiscountAppComponents.UsageLimitsCard.totalUsageLimitLabel',
),
value: UsageLimitType.TotalUsageLimit,
renderChildren: (isSelected: boolean) => (
<BlockStack>
{isSelected && (
<div className={styles.TotalUsageLimitTextField}>
<TextField
id={DISCOUNT_TOTAL_USAGE_LIMIT_FIELD}
label={i18n.translate(
'DiscountAppComponents.UsageLimitsCard.totalUsageLimitLabel',
)}
autoComplete="off"
labelHidden
value={totalUsageLimit.value || ''}
onChange={(nextValue) => {
totalUsageLimit.onChange(
forcePositiveInteger(nextValue),
);
}}
onBlur={totalUsageLimit.onBlur}
error={Boolean(totalUsageLimit.error)}
/>
</div>
)}
{isRecurring && (
<Text as="span" tone="subdued">
{i18n.translate(
'DiscountAppComponents.UsageLimitsCard.totalUsageLimitHelpTextSubscription',
)}
</Text>
)}
{isSelected && totalUsageLimit.error && (
<InlineError
fieldID={DISCOUNT_TOTAL_USAGE_LIMIT_FIELD}
message={totalUsageLimit.error}
/>
)}
</BlockStack>
),
},
{
label: i18n.translate(
'DiscountAppComponents.UsageLimitsCard.oncePerCustomerLimitLabel',
),
value: UsageLimitType.OncePerCustomerLimit,
},
]}
onChange={handleUsageLimitsChoicesChange}
/>
</BlockStack>
{isShowRecurringPaymentSection(props) && (
{isLimitedSection(props) && (
<BlockStack gap="400">
<UsageLimits
totalUsageLimit={props.totalUsageLimit}
oncePerCustomer={props.oncePerCustomer}
/>
</BlockStack>
)}
{isRecurringPaymentSection(props) && (
<BlockStack gap="400">
<RecurringPayment
recurringPaymentType={props.recurringPaymentType}
Expand All @@ -181,8 +85,14 @@ export function UsageLimitsCard(props: UsageLimitsCardProps) {
);
}

function isShowRecurringPaymentSection(
function isRecurringPaymentSection(
props: UsageLimitsCardProps,
): props is UsageLimitsCardMultiplePaymentsProps {
return Boolean(props.isRecurring);
}

function isLimitedSection(
props: UsageLimitsCardProps,
): props is UsageLimitsCardLimitsProps {
return Boolean(props.isLimited);
}
Loading
Loading