Skip to content

Commit 80734c5

Browse files
committed
WIP on new discounts tutorial app
1 parent bec3522 commit 80734c5

File tree

7 files changed

+345
-13
lines changed

7 files changed

+345
-13
lines changed

sample-apps/discounts/extensions/product-discount/shopify.function.extension.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ command = "cargo wasi build --release"
77
path = "target/wasm32-wasi/release/product-discount.wasm"
88

99
[ui.paths]
10-
create = "/"
11-
details = "/"
10+
create = "/volume/:functionId/new"
11+
details = "/volume/:functionId/:id"

sample-apps/discounts/web/frontend/App.jsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
AppBridgeProvider,
77
QueryProvider,
88
PolarisProvider,
9+
DiscountProvider,
910
} from "./components";
1011

1112
export default function App() {
@@ -17,17 +18,19 @@ export default function App() {
1718
<PolarisProvider>
1819
<BrowserRouter>
1920
<AppBridgeProvider>
20-
<QueryProvider>
21-
<NavigationMenu
22-
navigationLinks={[
23-
{
24-
label: "Page name",
25-
destination: "/pagename",
26-
},
27-
]}
28-
/>
29-
<Routes pages={pages} />
30-
</QueryProvider>
21+
<DiscountProvider>
22+
<QueryProvider>
23+
<NavigationMenu
24+
navigationLinks={[
25+
{
26+
label: "Page name",
27+
destination: "/pagename",
28+
},
29+
]}
30+
/>
31+
<Routes pages={pages} />
32+
</QueryProvider>
33+
</DiscountProvider>
3134
</AppBridgeProvider>
3235
</BrowserRouter>
3336
</PolarisProvider>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { AppProvider } from '@shopify/discount-app-components'
2+
import '@shopify/discount-app-components/build/esm/styles.css'
3+
4+
export function DiscountProvider({ children }) {
5+
return <AppProvider locale="en-US" ianaTimezone="America/Toronto">{children}</AppProvider>
6+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { AppBridgeProvider } from "./AppBridgeProvider";
22
export { QueryProvider } from "./QueryProvider";
33
export { PolarisProvider } from "./PolarisProvider";
4+
export { DiscountProvider } from './DiscountProvider';

sample-apps/discounts/web/frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
"@shopify/app-bridge": "^3.1.0",
1616
"@shopify/app-bridge-react": "^3.1.0",
1717
"@shopify/app-bridge-utils": "^3.1.0",
18+
"@shopify/discount-app-components": "^1.0.7",
1819
"@shopify/polaris": "^9.11.0",
20+
"@shopify/react-form": "^2.5.0",
21+
"@shopify/react-i18n": "^7.5.1",
1922
"@vitejs/plugin-react": "1.2.0",
2023
"react": "^17.0.2",
2124
"react-dom": "^17.0.2",
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { useParams } from "react-router-dom";
2+
import { useForm, useField } from "@shopify/react-form";
3+
import { CurrencyCode } from "@shopify/react-i18n";
4+
import { Redirect } from "@shopify/app-bridge/actions";
5+
import { useAppBridge } from "@shopify/app-bridge-react";
6+
7+
import {
8+
ActiveDatesCard,
9+
CombinationCard,
10+
DiscountClass,
11+
DiscountMethod,
12+
MethodCard,
13+
DiscountStatus,
14+
RequirementType,
15+
SummaryCard,
16+
UsageLimitsCard,
17+
onBreadcrumbAction,
18+
} from "@shopify/discount-app-components";
19+
import {
20+
Banner,
21+
Card,
22+
Layout,
23+
Page,
24+
TextField,
25+
Stack,
26+
PageActions,
27+
} from "@shopify/polaris";
28+
import { data } from "@shopify/app-bridge/actions/Modal";
29+
import { useAuthenticatedFetch } from "../../../hooks";
30+
31+
const todaysDate = new Date();
32+
const METAFIELD_NAMESPACE = "discounts-tutorial";
33+
const METAFIELD_CONFIGURATION_KEY = "volume-config";
34+
35+
export default function VolumeNew() {
36+
// Read the function ID from the URL
37+
const { functionId } = useParams();
38+
39+
const app = useAppBridge();
40+
const redirect = Redirect.create(app);
41+
const currencyCode = CurrencyCode.Cad;
42+
const authenticatedFetch = useAuthenticatedFetch();
43+
44+
const {
45+
fields: {
46+
discountTitle,
47+
discountCode,
48+
discountMethod,
49+
combinesWith,
50+
requirementType,
51+
requirementSubtotal,
52+
requirementQuantity,
53+
usageTotalLimit,
54+
usageOncePerCustomer,
55+
startDate,
56+
endDate,
57+
configuration,
58+
},
59+
submit,
60+
submitting,
61+
dirty,
62+
reset,
63+
submitErrors,
64+
makeClean,
65+
} = useForm({
66+
fields: {
67+
discountTitle: useField(""),
68+
discountMethod: useField(DiscountMethod.Code),
69+
discountCode: useField(""),
70+
combinesWith: useField({
71+
orderDiscounts: false,
72+
productDiscounts: false,
73+
shippingDiscounts: false,
74+
}),
75+
requirementType: useField(RequirementType.None),
76+
requirementSubtotal: useField("0"),
77+
requirementQuantity: useField("0"),
78+
usageTotalLimit: useField(null),
79+
usageOncePerCustomer: useField(false),
80+
startDate: useField(todaysDate),
81+
endDate: useField(null),
82+
configuration: {},
83+
},
84+
onSubmit: async (form) => {
85+
const discount = {
86+
functionId,
87+
combinesWith: form.combinesWith,
88+
startsAt: form.startDate,
89+
endsAt: form.endDate,
90+
metafields: [
91+
{
92+
namespace: METAFIELD_NAMESPACE,
93+
key: METAFIELD_CONFIGURATION_KEY,
94+
type: "json",
95+
value: JSON.stringify({}),
96+
},
97+
],
98+
};
99+
100+
let response;
101+
if (form.discountMethod === DiscountMethod.Automatic) {
102+
response = await authenticatedFetch("/api/discounts/automatic", {
103+
method: "POST",
104+
headers: { "Content-Type": "application/json" },
105+
body: JSON.stringify({ ...discount, title: form.discountTitle }),
106+
});
107+
} else {
108+
response = await authenticatedFetch("/api/discounts/code", {
109+
method: "POST",
110+
headers: { "Content-Type": "application/json" },
111+
body: JSON.stringify({
112+
...discount,
113+
title: form.discountCode,
114+
code: form.discountCode,
115+
}),
116+
});
117+
}
118+
119+
const data = (await response.json()).data;
120+
const remoteErrors = data.discountCreate.userErrors;
121+
if (remoteErrors.length > 0) {
122+
return { status: "fail", errors: remoteErrors };
123+
}
124+
125+
redirect.dispatch(Redirect.Action.ADMIN_SECTION, {
126+
name: Redirect.ResourceType.Discount,
127+
});
128+
return { status: "success" };
129+
},
130+
});
131+
132+
const errorBanner =
133+
submitErrors.length > 0 ? (
134+
<Layout.Section>
135+
<Banner status="critical">
136+
<p>There were some issues with your form submission:</p>
137+
<ul>
138+
{submitErrors.map(({ message, field }, index) => {
139+
return (
140+
<li key={`${message}${index}`}>
141+
{field.join(".")} {message}
142+
</li>
143+
);
144+
})}
145+
</ul>
146+
</Banner>
147+
</Layout.Section>
148+
) : null;
149+
150+
return (
151+
<Page
152+
title="Create volume discount"
153+
breadcrumbs={[
154+
{
155+
content: "Discounts",
156+
onAction: () => onBreadcrumbAction(redirect, true),
157+
},
158+
]}
159+
primaryAction={{
160+
content: "Save",
161+
onAction: submit,
162+
disabled: !dirty,
163+
loading: submitting,
164+
}}
165+
>
166+
<Layout>
167+
{errorBanner}
168+
<Layout.Section>
169+
<form onSubmit={submit}>
170+
<MethodCard
171+
title="Volume"
172+
discountTitle={discountTitle}
173+
discountClass={DiscountClass.Product}
174+
discountCode={discountCode}
175+
discountMethod={discountMethod}
176+
/>
177+
{discountMethod.value === DiscountMethod.Code && (
178+
<UsageLimitsCard
179+
totalUsageLimit={usageTotalLimit}
180+
oncePerCustomer={usageOncePerCustomer}
181+
/>
182+
)}
183+
<CombinationCard
184+
combinableDiscountTypes={combinesWith}
185+
discountClass={DiscountClass.Product}
186+
discountDescriptor={
187+
discountMethod.value === DiscountMethod.Automatic
188+
? discountTitle.value
189+
: discountCode.value
190+
}
191+
/>
192+
<ActiveDatesCard
193+
startDate={startDate}
194+
endDate={endDate}
195+
timezoneAbbreviation="EST"
196+
/>
197+
</form>
198+
</Layout.Section>
199+
<Layout.Section secondary>
200+
<SummaryCard
201+
header={{
202+
discountMethod: discountMethod.value,
203+
discountDescriptor:
204+
discountMethod.value === DiscountMethod.Automatic
205+
? discountTitle.value
206+
: discountCode.value,
207+
appDiscountType: "Volume",
208+
isEditing: false,
209+
}}
210+
performance={{
211+
status: DiscountStatus.Scheduled,
212+
usageCount: 0,
213+
}}
214+
minimumRequirements={{
215+
requirementType: requirementType.value,
216+
subtotal: requirementSubtotal.value,
217+
quantity: requirementQuantity.value,
218+
currencyCode: currencyCode,
219+
}}
220+
usageLimits={{
221+
oncePerCustomer: usageOncePerCustomer.value,
222+
totalUsageLimit: usageTotalLimit.value,
223+
}}
224+
activeDates={{
225+
startDate: startDate.value,
226+
endDate: endDate.value,
227+
}}
228+
/>
229+
</Layout.Section>
230+
<Layout.Section>
231+
<PageActions
232+
primaryAction={{
233+
content: "Save discount",
234+
onAction: submit,
235+
disabled: !dirty,
236+
loading: submitting,
237+
}}
238+
secondaryActions={[
239+
{
240+
content: "Discard",
241+
onAction: () => onBreadcrumbAction(redirect, true),
242+
},
243+
]}
244+
/>
245+
</Layout.Section>
246+
</Layout>
247+
</Page>
248+
);
249+
}

0 commit comments

Comments
 (0)