Skip to content

Commit c0d5e1e

Browse files
authored
Merge pull request #150 from Shopify/ah.add-edit-pages
Add payment and delivery details pages
2 parents a12a4fb + e0386c4 commit c0d5e1e

File tree

4 files changed

+592
-0
lines changed
  • sample-apps
    • delivery-customizations/web
    • payment-customizations/web

4 files changed

+592
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { useState, useEffect } from "react";
2+
import { useParams } from "react-router-dom";
3+
import {
4+
Layout,
5+
Form,
6+
FormLayout,
7+
TextField,
8+
Card,
9+
Page,
10+
Frame,
11+
SkeletonPage,
12+
SkeletonBodyText,
13+
} from "@shopify/polaris";
14+
import { useAppBridge } from "@shopify/app-bridge-react";
15+
import { Redirect } from "@shopify/app-bridge/actions";
16+
import { useAuthenticatedFetch } from "../../../hooks/useAuthenticatedFetch";
17+
import { useAppQuery } from "../../../hooks/useAppQuery";
18+
19+
// Utility hooks for invoking the server endpoints that you created
20+
function useCustomization(id) {
21+
const url = `/api/deliveryCustomization/${id}`;
22+
return useAppQuery({ url });
23+
}
24+
25+
function useUpdateCustomization() {
26+
const fetch = useAuthenticatedFetch();
27+
return async (deliveryCustomization) => {
28+
return await fetch("/api/deliveryCustomization/update", {
29+
method: "PUT",
30+
headers: { "Content-Type": "application/json" },
31+
body: JSON.stringify(deliveryCustomization),
32+
});
33+
};
34+
}
35+
36+
// A utility hook for redirecting back to the Delivery Customizations list
37+
function useRedirectToCustomizations() {
38+
const app = useAppBridge();
39+
const redirect = Redirect.create(app);
40+
return () => {
41+
redirect.dispatch(Redirect.Action.ADMIN_PATH, {
42+
path: "/settings/shipping/customizations",
43+
});
44+
};
45+
}
46+
47+
export default function NewCustomizationPage() {
48+
// Read the function and customization IDs from the URL
49+
const { functionId, id } = useParams();
50+
51+
// Fetch customization data
52+
const { isLoading, data: customization } = useCustomization(id);
53+
54+
// Utility hooks
55+
const updateCustomization = useUpdateCustomization();
56+
const redirect = useRedirectToCustomizations();
57+
58+
// Page state management
59+
const [mutationIsLoading, setMutationIsLoading] = useState(false);
60+
const [formData, setFormData] = useState({});
61+
62+
// Page breadcrumbs [
63+
const breadcrumbs = [
64+
{
65+
content: "Delivery Customizations",
66+
onAction: redirect,
67+
},
68+
];
69+
70+
// Store the form state on change
71+
const handleInputChange = (value, name) => {
72+
setFormData((data) => ({ ...data, [name]: value }));
73+
};
74+
75+
// Invoke the server endpoint when the form is submitted
76+
const handleSubmit = async () => {
77+
setMutationIsLoading(true);
78+
const response = await updateCustomization({
79+
functionId,
80+
...formData,
81+
});
82+
if (response.status != 200) {
83+
const errorResponse = await response.json();
84+
console.log(
85+
"Error updating delivery customization: ",
86+
errorResponse.error
87+
);
88+
} else {
89+
redirect();
90+
}
91+
setMutationIsLoading(false);
92+
};
93+
94+
// When the customization is loaded, set the form state
95+
useEffect(() => {
96+
setFormData(customization);
97+
}, [customization, setFormData]);
98+
99+
if (isLoading) {
100+
return (
101+
<Frame>
102+
<SkeletonPage
103+
title="Add message to delivery options"
104+
breadcrumbs={breadcrumbs}
105+
primaryAction
106+
>
107+
<Layout.Section>
108+
<Card sectioned>
109+
<SkeletonBodyText />
110+
</Card>
111+
</Layout.Section>
112+
</SkeletonPage>
113+
</Frame>
114+
);
115+
}
116+
117+
// A basic input form page created using Polaris components
118+
return (
119+
<Frame>
120+
<Page
121+
title="Add message to delivery options"
122+
primaryAction={{
123+
onAction: handleSubmit,
124+
content: "Save",
125+
loading: mutationIsLoading,
126+
}}
127+
breadcrumbs={breadcrumbs}
128+
>
129+
<Layout.Section>
130+
<Card>
131+
<Card.Section>
132+
<Form onSubmit={handleSubmit}>
133+
<FormLayout>
134+
<FormLayout.Group condensed>
135+
<TextField
136+
type="text"
137+
maxLength={2}
138+
label="State/Province Code"
139+
value={formData?.stateProvinceCode}
140+
onChange={(value) =>
141+
handleInputChange(value, "stateProvinceCode")
142+
}
143+
disabled={isLoading}
144+
requiredIndicator
145+
/>
146+
</FormLayout.Group>
147+
<FormLayout.Group>
148+
<TextField
149+
type="text"
150+
multiline={2}
151+
label="Message"
152+
value={formData?.message}
153+
onChange={(value) => handleInputChange(value, "message")}
154+
disabled={isLoading}
155+
requiredIndicator
156+
/>
157+
</FormLayout.Group>
158+
</FormLayout>
159+
</Form>
160+
</Card.Section>
161+
</Card>
162+
</Layout.Section>
163+
</Page>
164+
</Frame>
165+
);
166+
}

sample-apps/delivery-customizations/web/index.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,133 @@ function handleUserError(userErrors, res) {
4545
return false;
4646
}
4747

48+
// Endpoint for fetching a delivery customization
49+
app.get("/api/deliveryCustomization/:id", async (req, res) => {
50+
const id = `gid://shopify/DeliveryCustomization/${req.params.id}`;
51+
const graphqlClient = new shopify.api.clients.Graphql({
52+
session: res.locals.shopify.session,
53+
});
54+
55+
try {
56+
const response = await graphqlClient.query({
57+
data: {
58+
query: `query DeliveryCustomization($id: ID!) {
59+
deliveryCustomization(id: $id) {
60+
id
61+
metafield(namespace: "delivery-customization", key: "function-configuration") {
62+
value
63+
}
64+
}
65+
}`,
66+
variables: {
67+
id,
68+
},
69+
},
70+
});
71+
72+
if (!response.body.data || !response.body.data.deliveryCustomization) {
73+
res.status(404).send({ error: "Delivery customization not found" });
74+
}
75+
76+
const metafieldValue = response.body.data.deliveryCustomization.metafield
77+
? JSON.parse(response.body.data.deliveryCustomization.metafield.value)
78+
: {};
79+
const { stateProvinceCode, message } = metafieldValue;
80+
81+
const deliveryCustomization = {
82+
id,
83+
stateProvinceCode,
84+
message,
85+
};
86+
87+
res.status(200).send(deliveryCustomization);
88+
} catch (error) {
89+
console.error(`Failed to fetch delivery customization ${id}`, error);
90+
res.status(500).send();
91+
}
92+
});
93+
94+
app.put("/api/deliveryCustomization/update", async (req, res) => {
95+
const payload = req.body;
96+
const graphqlClient = new shopify.api.clients.Graphql({
97+
session: res.locals.shopify.session,
98+
});
99+
100+
try {
101+
// Create the delivery customization for the provided function ID
102+
const updateResponse = await graphqlClient.query({
103+
data: {
104+
query: `mutation DeliveryCustomizationUpdate($id: ID!, $input: DeliveryCustomizationInput!) {
105+
deliveryCustomizationUpdate(id: $id, deliveryCustomization: $input) {
106+
deliveryCustomization {
107+
id
108+
}
109+
userErrors {
110+
message
111+
}
112+
}
113+
}`,
114+
variables: {
115+
input: {
116+
functionId: payload.functionId,
117+
title: `Display message for ${payload.stateProvinceCode}`,
118+
enabled: true,
119+
},
120+
id: payload.id,
121+
},
122+
},
123+
});
124+
let updateResult = updateResponse.body.data.deliveryCustomizationUpdate;
125+
if (handleUserError(updateResult.userErrors, res)) {
126+
return;
127+
}
128+
129+
// Populate the function configuration metafield for the delivery customization
130+
const customizationId = updateResult.deliveryCustomization.id;
131+
const metafieldResponse = await graphqlClient.query({
132+
data: {
133+
query: `mutation MetafieldsSet($customizationId: ID!, $configurationValue: String!) {
134+
metafieldsSet(metafields: [
135+
{
136+
ownerId: $customizationId
137+
namespace: "delivery-customization"
138+
key: "function-configuration"
139+
value: $configurationValue
140+
type: "json"
141+
}
142+
]) {
143+
metafields {
144+
id
145+
}
146+
userErrors {
147+
message
148+
}
149+
}
150+
}`,
151+
variables: {
152+
customizationId,
153+
configurationValue: JSON.stringify({
154+
stateProvinceCode: payload.stateProvinceCode,
155+
message: payload.message,
156+
}),
157+
},
158+
},
159+
});
160+
let metafieldResult = metafieldResponse.body.data.metafieldsSet;
161+
if (handleUserError(metafieldResult, res)) {
162+
return;
163+
}
164+
} catch (error) {
165+
// Handle errors thrown by the graphql client
166+
if (!(error instanceof GraphqlQueryError)) {
167+
throw error;
168+
}
169+
return res.status(500).send({ error: error.response });
170+
}
171+
172+
return res.status(200).send();
173+
});
174+
48175
// Endpoint for the delivery customization UI to invoke
49176
app.post("/api/deliveryCustomization/create", async (req, res) => {
50177
const payload = req.body;

0 commit comments

Comments
 (0)