Skip to main content

Create the UI for a pre-order and Try Before You Buy (TBYB) app

Previously, you set up the foundation of your app. You're now ready to add app components that can help merchants create their pre-orders.

On the app page, you want to add functionality that enables merchants to do the following tasks:

  • Set the pre-order name
  • Configure the delivery, inventory, and billing policies for the pre-order
  • Select products that will have the pre-order

You'll use Polaris and App Bridge React to build the user interface. You'll use GraphQL Admin API mutations and queries to create and retrieve pre-orders.

At the end of this tutorial, you'll have an understanding on how to create pre-orders. This app will be simple, but you'll learn where to find resources to build more complex features on your own.

Note

The sample app in this tutorial is assigned with a PRE_ORDER category. When you create your own app, you can assign a category based on the app's purpose. For more information, refer to Selling plans category.


In this tutorial, you'll learn how to do the following tasks:

  • Add new routes to your app's server that call the Shopify Admin API
  • Add UI to create a pre-order
  • Test your app on a development store

This tutorial is not intended to give you a full example on how to build a pre-order application, but more a reference starter.



Anchor to Step 1: Create the pre-order creation routeStep 1: Create the pre-order creation route

Create a file app.create.jsx under app/routes. This file will create the route /app/create that will let you create a pre-order.

app/routes/app.create.jsx

Create pre-order route

import { json } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { useState } from "react";
import { authenticate } from "../shopify.server";
import db from "../db.server";

export const action = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const formData = await request.formData();

const title = formData.get("title");
const productId = formData.get("productId");
const variantId = formData.get("variantId");
const startDate = formData.get("startDate");
const endDate = formData.get("endDate");

if (!title || !productId || !variantId || !startDate || !endDate) {
return json(
{ error: "All fields are required" },
{ status: 400 }
);
}

try {
await db.preOrder.create({
data: {
title,
productId,
variantId,
startDate: new Date(startDate),
endDate: new Date(endDate),
shopDomain: admin.shop.domain,
},
});

return json({ success: true });
} catch (error) {
return json({ error: error.message }, { status: 500 });
}
};

export default function CreatePreOrder() {
const nav = useNavigation();
const actionData = useActionData();
const [selectedProduct, setSelectedProduct] = useState(null);
const [selectedVariant, setSelectedVariant] = useState(null);

const isLoading = nav.state === "submitting";

return (
<div className="p-4 md:p-8">
<h1 className="text-2xl font-bold mb-4">Create Pre-order</h1>
<Form method="post">
<div className="space-y-4">
<div>
<label htmlFor="title" className="block mb-2">
Pre-order Title
</label>
<input
type="text"
id="title"
name="title"
className="w-full p-2 border rounded"
required
/>
</div>

<div>
<label htmlFor="startDate" className="block mb-2">
Start Date
</label>
<input
type="datetime-local"
id="startDate"
name="startDate"
className="w-full p-2 border rounded"
required
/>
</div>

<div>
<label htmlFor="endDate" className="block mb-2">
End Date
</label>
<input
type="datetime-local"
id="endDate"
name="endDate"
className="w-full p-2 border rounded"
required
/>
</div>

<div>
<label htmlFor="productId" className="block mb-2">
Product
</label>
<input
type="text"
id="productId"
name="productId"
className="w-full p-2 border rounded"
required
/>
</div>

<div>
<label htmlFor="variantId" className="block mb-2">
Variant
</label>
<input
type="text"
id="variantId"
name="variantId"
className="w-full p-2 border rounded"
required
/>
</div>

{actionData?.error && (
<div className="text-red-500">{actionData.error}</div>
)}

<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:bg-blue-300"
disabled={isLoading}
>
{isLoading ? "Creating..." : "Create Pre-order"}
</button>
</div>
</Form>
</div>
);
}

This code creates a basic form to create pre-orders. The form includes fields for:

  • Pre-order title
  • Start date
  • End date
  • Product ID
  • Variant ID

When submitted, it will:

  1. Validate that all required fields are present
  2. Create a new pre-order record in the database
  3. Show success/error messages accordingly
Info

For now, we're using simple text inputs for product and variant IDs. In a later step, we'll add proper product selection using the Shopify API.


Anchor to Step 2: Create app componentsStep 2: Create app components

Create the following app components:

Tip

We recommend keeping your app component files in the app/components folder.

Anchor to Create the name componentCreate the name component

Create a component that enables merchants to set the pre-order name. The name component includes the following Polaris components:

A screen capture showing the name component

In your app/routes/app.create.jsx file, add the following code:

app/routes/app.create.jsx

import { useState } from "react";
import {
Page,
Card,
BlockStack,
Layout,
TextField,
FormLayout,
} from "@shopify/polaris";

export default function Index() {
const [sellingPlanName, setSellingPlanName] = useState("");

return (
<Page>
<ui-title-bar title="Create a pre-order" />
<Layout>
<Layout.Section>
<BlockStack gap="400">
<FormLayout>
<Card>
<BlockStack gap="200">
<TextField
label="Pre-order name"
name="sellingPlanName"
value={sellingPlanName}
onChange={setSellingPlanName}
/>
</BlockStack>
</Card>
</FormLayout>
</BlockStack>
</Layout.Section>
</Layout>
</Page>
);
}

Anchor to Create the checkout charge componentCreate the checkout charge component

When merchants create a pre-order, they can decide the initial charge when customers check out. The type of charge can be set with a PERCENTAGE or PRICE type. Learn more about selling plan checkout charge types and selling plan checkout charge values.

The checkout charge component includes the following Polaris components:

A screen capture showing the deposit component

In the app/components folder, create the file checkoutCharge.jsx.

app/components/CheckoutCharge.jsx

import {
Text,
Card,
BlockStack,
DatePicker,
TextField,
} from "@shopify/polaris";
import { useState } from "react";

export default function CheckoutCharge({
selectedDates,
setSelectedDates,
initialCheckoutCharge,
setInitialCheckoutCharge,
}) {
const today = new Date();
const [{ month, year }, setDate] = useState({
month: today.getMonth(),
year: today.getFullYear(),
});

const handleMonthChange = (month, year) => setDate({ month, year });

return (
<Card>
<BlockStack gap="400">
<TextField
label="Initial deposit"
name="checkoutCharge"
type="number"
suffix="%"
value={initialCheckoutCharge}
onChange={setInitialCheckoutCharge}
max={100}
/>
{initialCheckoutCharge < 100 && (
<BlockStack gap="400">
<Text as="label">Remaining balance charge date</Text>
<DatePicker
month={month}
year={year}
onChange={setSelectedDates}
onMonthChange={handleMonthChange}
selected={selectedDates}
/>
</BlockStack>
)}
</BlockStack>
</Card>
);
}

Anchor to Create the product picker componentCreate the product picker component

Create a component that enables merchants to select products that will have the pre-order. The product selection component includes the Resource Picker API.

A screen capture showing the product selection component

In the app/components folder, create the file ProductPicker.jsx.

app/components/ProductPicker.jsx

import { Link } from "@remix-run/react";
import {
Text,
Card,
BlockStack,
Button,
TextField,
Icon,
InlineStack,
Thumbnail,
Box,
} from "@shopify/polaris";
import { SearchIcon, ImageIcon } from "@shopify/polaris-icons";

export default function ProductPicker({
selectedProducts,
setSelectedProducts,
}) {
async function selectProducts(selectedProducts, searchQuery) {
const selectedItems = await window.shopify.resourcePicker({
selectionIds: selectedProducts,
multiple: true,
query: searchQuery,
type: "product",
action: "select",
});

if (selectedItems) {
setSelectedProducts(selectedItems);
}
}

return (
<Card>
<BlockStack gap="400">
<InlineStack gap="400" align="start">
<div style={{ flexGrow: 1 }}>
<TextField
prefix={<Icon source={SearchIcon} />}
type="search"
id="productSearch"
name="productSearch"
placeholder="Search products"
autoComplete="off"
value={""}
/>
</div>
<Button
onClick={() => {
selectProducts([], "");
}}
>
Browse
</Button>
</InlineStack>
{selectedProducts && selectedProducts.length ? (
<BlockStack gap="400">
{selectedProducts.map(
({ id, images, variants, title, totalVariants }, index) => {
const hasImage = images && images.length;

return (
<InlineStack
key={`${id}-${index}`}
gap="400"
blockAlign="center"
wrap={false}
>
<Box width="1500">
<Thumbnail
source={hasImage ? images[0].originalSrc : ImageIcon}
alt={
hasImage && images[0].altText ? images[0].altText : ""
}
size="medium"
/>
</Box>
<div style={{ flexGrow: 1 }}>
<BlockStack>
<Text as="span">{title}</Text>
{totalVariants !== undefined ? (
<Text as="span" tone="subdued">
({variants?.length || totalVariants} of{" "}
{totalVariants} variants selected)
</Text>
) : null}
</BlockStack>
</div>
<Link
removeUnderline
onClick={() => selectProducts(selectedProducts)}
>
Edit
</Link>
</InlineStack>
);
}
)}
</BlockStack>
) : null}
</BlockStack>
</Card>
);
}

Anchor to Step 3: Action to create the pre-orderStep 3: Action to create the pre-order

The next step is to create the action that will make it possible to create the pre-order with all the information we are gathering with the previous created components. Add the action inside your app/routes/app.create.jsx file. The following action is currently showing a working example on how to use the sellingPlanGroupCreate mutation to create a pre-order, this example can also be updated to create a Try before you buy.

app/routes/app.create.jsx

export const action = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const form = await request.formData();
const sellingPlanName = form.get("sellingPlanName");
const initialCheckoutCharge = form.get("initialCheckoutCharge");
const selectedProducts = form.get("selectedProducts");
const selectedProductsArray = selectedProducts
? selectedProducts.split(",")
: [];
const selectedDates = form.get("selectedDates");
const haveRemainingBalance = Number(initialCheckoutCharge) < 100;
const response = await admin.graphql(
`#graphql
mutation sellingPlanGroupCreate($input: SellingPlanGroupInput!, $resources: SellingPlanGroupResourceInput!) {
sellingPlanGroupCreate(input: $input, resources: $resources) {
sellingPlanGroup {
id
}
userErrors {
field
message
}
}
}`,
{
variables: {
input: {
name: sellingPlanName,
merchantCode: "Pre-order",
options: ["pre-order"],
position: 1,
sellingPlansToCreate: [
{
name: "Pre-order with deposit",
options: "Pre-order with deposit",
category: "PRE_ORDER",
billingPolicy: {
fixed: {
checkoutCharge: {
type: "PERCENTAGE",
value: {
percentage: Number(initialCheckoutCharge),
},
},
remainingBalanceChargeTrigger: haveRemainingBalance
? "EXACT_TIME"
: "NO_REMAINING_BALANCE",
remainingBalanceChargeExactTime: haveRemainingBalance
? new Date(selectedDates).toISOString()
: null,
},
},
deliveryPolicy: {
fixed: {
fulfillmentTrigger: "UNKNOWN",
},
},
inventoryPolicy: {
reserve: "ON_FULFILLMENT",
},
},
],
},
resources: {
productIds: selectedProductsArray,
},
},
},
);
const responseJson = await response.json();

return json({
sellingPlanGroup:
responseJson.data?.sellingPlanGroupCreate?.sellingPlanGroup?.id,
});
};

app/routes/app.create.jsx

import { useState, useEffect } from "react";
import { json } from "@remix-run/node";
import { useActionData, useNavigation, useSubmit } from "@remix-run/react";
import {
Page,
Card,
BlockStack,
Button,
PageActions,
Layout,
TextField,
FormLayout,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
import ProductPicker from "../components/ProductPicker";
import CheckoutCharge from "../components/CheckoutCharge";

export const action = async ({ request }) => {
const { admin } = await authenticate.admin(request);
const form = await request.formData();
const sellingPlanName = form.get("sellingPlanName");
const initialCheckoutCharge = form.get("initialCheckoutCharge");
const selectedProducts = form.get("selectedProducts");
const selectedProductsArray = selectedProducts
? selectedProducts.split(",")
: [];
const selectedDates = form.get("selectedDates");
const haveRemainingBalance = Number(initialCheckoutCharge) &lt; 100;
const response = await admin.graphql(
`#graphql
mutation sellingPlanGroupCreate($input: SellingPlanGroupInput!, $resources: SellingPlanGroupResourceInput!) {
sellingPlanGroupCreate(input: $input, resources: $resources) {
sellingPlanGroup {
id
}
userErrors {

The code that you just created is only a first step to create a complete pre-order or Try before you buy app. You can use the following API objects to improve your application:


Was this page helpful?