Merchants create and manage payment customizations in the Shopify admin. To render the payment customization creation and editing experience for the merchant, Shopify uses [URLs that you configure](/docs/apps/build/functions/input-output/metafields-for-input-queries#creating-your-merchant-interface). You can customize this UI for your function's configuration needs, or to meet other requirements of your app.
## What you'll learn
In this tutorial, you'll learn how to do the following tasks:
- Create an App Bridge UI that enables users to create a [function owner](/docs/apps/build/functions/input-output/metafields-for-input-queries#how-it-works).
- Configure the UI paths for your function.

## Requirements
- You've completed the [Add configuration to your payment customization](/docs/apps/build/checkout/payments/add-configuration) tutorial.
- You created your app with the [Remix app template](/docs/api#app-templates).
## Step 1: Create the frontend UI for your function
The following example builds a React-based page that enables merchants to create and configure a new payment customization. The code renders a frontend page in your app and uses the GraphQL Admin API to create a payment customization.
1. In `app/routes`, create a new file named `app.payment-customization.$functionId.$id.jsx`.
The Shopify Remix app template uses file-based routing, so the file name determines the page's URL. The `$` prefix indicates `functionId` and `id` are [dynamic segments](https://remix.run/docs/en/main/guides/routing#dynamic-segments). The path for this page is `/app/payment-customization/{functionId}/{id}` .
1. Add the following code in `app.payment-customization.$functionId.$id.jsx`:
- The [`loader`](https://remix.run/docs/en/main/route/loader) function handles fetching the data to populate the form and is used when this page has an `id` value that is not `new`.
- The [`action`](https://remix.run/docs/en/main/route/action) function handles submitting the form data to Shopify to create the payment customization.
- The `PaymentCustomization` function renders the page and form components using [Polaris components](https://polaris.shopify.com/components) and [Remix hooks](https://remix.run/docs/en/main/hooks/use-navigation).
```javascript
import { useState, useEffect } from "react";
import {
Banner,
Button,
Card,
FormLayout,
Layout,
Page,
TextField,
} from "@shopify/polaris";
import {
Form,
useActionData,
useNavigation,
useSubmit,
useLoaderData,
} from "@remix-run/react";
import { json } from "@remix-run/node";
import { authenticate } from "../shopify.server";
// This is a server-side function that provides data to the component when rendering.
export const loader = async ({ params, request }) => {
const { id } = params;
// If the ID is `new`, then we are creating a new customization and there's no data to load.
if (id === "new") {
return {
paymentMethodName: "",
cartTotal: "0",
};
}
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(
`#graphql
query getPaymentCustomization($id: ID!) {
paymentCustomization(id: $id) {
id
metafield(namespace: "$app:payment-customization", key: "function-configuration") {
value
}
}
}`,
{
variables: {
id: `gid://shopify/PaymentCustomization/${id}`,
},
}
);
const responseJson = await response.json();
const metafield =
responseJson.data.paymentCustomization?.metafield?.value &&
JSON.parse(responseJson.data.paymentCustomization.metafield.value);
return json({
paymentMethodName: metafield?.paymentMethodName ?? "",
cartTotal: metafield?.cartTotal ?? "0",
});
};
// This is a server-side action that is invoked when the form is submitted.
// It makes an admin GraphQL request to create a payment customization.
export const action = async ({ params, request }) => {
const { functionId, id } = params;
const { admin } = await authenticate.admin(request);
const formData = await request.formData();
const paymentMethodName = formData.get("paymentMethodName");
const cartTotal = parseFloat(formData.get("cartTotal"));
const paymentCustomizationInput = {
functionId,
title: `Hide ${paymentMethodName} if cart total is larger than ${cartTotal}`,
enabled: true,
metafields: [
{
namespace: "$app:payment-customization",
key: "function-configuration",
type: "json",
value: JSON.stringify({
paymentMethodName,
cartTotal,
}),
},
],
};
// If the ID is `new`, then we're creating a new customization. Otherwise, we will use the update mutation.
if (id === "new") {
const response = await admin.graphql(
`#graphql
mutation createPaymentCustomization($input: PaymentCustomizationInput!) {
paymentCustomizationCreate(paymentCustomization: $input) {
paymentCustomization {
id
}
userErrors {
message
}
}
}`,
{
variables: {
input: paymentCustomizationInput,
},
}
);
const responseJson = await response.json();
const errors = responseJson.data.paymentCustomizationCreate?.userErrors;
return json({ errors });
} else {
const response = await admin.graphql(
`#graphql
mutation updatePaymentCustomization($id: ID!, $input: PaymentCustomizationInput!) {
paymentCustomizationUpdate(id: $id, paymentCustomization: $input) {
paymentCustomization {
id
}
userErrors {
message
}
}
}`,
{
variables: {
id: `gid://shopify/PaymentCustomization/${id}`,
input: paymentCustomizationInput,
},
}
);
const responseJson = await response.json();
const errors = responseJson.data.paymentCustomizationUpdate?.userErrors;
return json({ errors });
}
};
// This is the client-side component that renders the form.
export default function PaymentCustomization() {
const submit = useSubmit();
const actionData = useActionData();
const navigation = useNavigation();
const loaderData = useLoaderData();
const [paymentMethodName, setPaymentMethodName] = useState(
loaderData.paymentMethodName
);
const [cartTotal, setCartTotal] = useState(loaderData.cartTotal);
const isLoading = navigation.state === "submitting";
const errorBanner = actionData?.errors.length ? (
{actionData?.errors.map((error, index) => {
return