--- title: Build an alternative payments extension description: Learn how to build a Shopify alternative payments extension using Polaris and Prisma. source_url: html: https://shopify.dev/docs/apps/build/payments/alternative/build-an-alternative-payment-extension?framework=remix md: https://shopify.dev/docs/apps/build/payments/alternative/build-an-alternative-payment-extension.md?framework=remix --- # Build an alternative payments extension Outdated This tutorial uses an outdated API version for building Checkout UI extensions. We recommend using `2025-10` to make use of new [Polaris web components](https://shopify.dev/docs/beta/next-gen-dev-platform/polaris). Beta Processing a payment with an alternative payments extension is currently in an invite-only closed beta. Tip Ensure you have the [latest version of Shopify CLI](https://shopify.dev/docs/api/shopify-cli#upgrade) installed to access all payment extension features. Alternative payments extensions allow Partners to incorporate additional fields and gather all necessary information upfront, directly on the checkout page, before initiating a payment. This provides your payments extension with the ability to finalize a customer's payment without having to redirect them to an offsite page, yet if required, can still redirect them similar to the offsite payments extension. Alternative payments extensions also enable Partners to prompt buyers to complete an additional verification step, if necessary, after they click Pay Now. This is an improvement to the existing flow where buyers are redirected to an offsite page to complete verification challenges. ## What you'll learn In this tutorial, you'll learn how to do the following tasks: * Set up your app. * Create a checkout UI extension. * Create an alternative payments extension. * Explore the payment, refund, void, reject, and capture session flows, as well as how to implement them yourself. * Implement buyer verification challenges using payment session modals. ## Requirements [Create a Partner account](https://www.shopify.com/partners) [Create a development store](https://shopify.dev/docs/apps/tools/development-stores) The development store should be pre-populated with test data. [Become a Payments Partner](https://shopify.dev/docs/apps/build/payments/payments-extension-review#payments-partner-application-review) Apply and receive approval to become a Payments Partner. ## Project ![](https://shopify.dev/images/logos/Remix.svg)![](https://shopify.dev/images/logos/Remix-dark.svg) Remix [View on GitHub](https://github.com/Shopify/example-app--payments-app-template--remix/blob/main-js) ## Scaffold an app Create a new payments app using [Shopify CLI](https://shopify.dev/docs/api/shopify-cli). ### Scaffold an app using Shopify CLI 1. Run the following command to start creating your app: ## Terminal ```bash npm init @shopify/app@latest ``` 2. When prompted, enter the name of your app. 3. When prompted for the approach, select the option to add your first extension. ## Terminal ```bash Build a Remix app (recommended) > Build an extension-only app ``` ## Create a checkout UI extension Once the skeleton app has been created, create and publish checkout UI extension for your app. 1. Follow these [steps](https://shopify.dev/docs/api/checkout-ui-extensions) to create your checkout UI extension for your app. 2. After [generating your extension](https://shopify.dev/docs/api/shopify-cli/app/app-generate-extension), the only difference is that the [extension point](https://shopify.dev/docs/api/checkout-ui-extensions/unstable/extension-targets-overview) you will bind to, and the hook you will call to update attributes from your extension, are not listed in the public documentation. A [sample checkout UI extension](https://shopify.dev/docs/apps/build/payments/alternative/build-an-alternative-payment-extension#sample-checkout-ui-extension) shown below is done in Typescript/React. This framework is recommended when generating the extension. 3. Once you have written your checkout UI extension, [deploy](https://shopify.dev/docs/api/shopify-cli/app/app-deploy) it to partners. ### Sample Checkout UI Extension **Note**: Please ensure you have the latest version of `checkout-ui-extensions-react`, as well as the latest version of `checkout-ui-extensions`, as the default package installation will ship with a version that does not contain `useApplyPaymentMethodAttributesChange` and `PaymentMethodAttributesUpdateChange`. Here is a sample of what a checkout UI extension could look like for a Custom Payment Method payments app (this would be within your index.tsx file of your checkout UI extension): ## Sample Checkout UI Extension ```ts import { render, Form, Grid, useApplyPaymentMethodAttributesChange, View, TextField, } from '@shopify/ui-extensions-react/checkout'; import {PaymentMethodAttributesUpdateChange} from '@shopify/ui-extensions/checkout'; import {useState, useEffect} from 'react'; render('purchase.checkout.payment-option-item.details.render', () => ); const LOG_PREFIX = '[Checkout UI Extension]'; function Extension() { const [bankName, setBankName] = useState(''); const [bankNumber, setBankNumber] = useState(''); const applyPaymentMethodAttributesChange = useApplyPaymentMethodAttributesChange(); const change = { type: 'updatePaymentMethodAttributes', attributes: [ {key: 'bank_name', value: bankName}, {key: 'bank_number', value: bankNumber}, ], } as PaymentMethodAttributesUpdateChange; useEffect(() => { applyPaymentMethodAttributesChange(change) .then((result) => { console.log(`${LOG_PREFIX} Applied change`, change, result); }) .catch((error) => { console.error(`${LOG_PREFIX} Failed to apply`, change, error); }); }, [change]); return (
applyPaymentMethodAttributesChange(change)}> setBankName(newValue)} /> setBankNumber(newValue)} /> ); } ``` **Note**: You must also specify the extension point you are rendering to (`purchase.checkout.payment-option-item.details.render`) in the `shopify.extension.toml` file. The standardAPI is a good starting point to see what is available for your checkout extension. Here is a [demo](https://drive.google.com/file/d/1DHciwok7MM-ak94x3sfQJ4OqJ_yKeHWc/view?usp=sharing) on how to build a new checkout UI extension using the CLI and deploying it to an existing app within the dashboard. ## Create a payments extension Your Shopify app becomes a payments app after you've created and configured your payments extension. 1. Run the following command to start generating your payment extension: ## Terminal ```bash npm shopify app generate extension ``` 2. When prompted, choose your organization & create this as a new app. 3. When prompted for "Type of extension", select "Payments App Extension > Custom Onsite" and name your extension. ## Configure your payments extension When you [generate an app extension](https://shopify.dev/docs/api/shopify-cli/app/app-generate-extension), a TOML configuration file named `shopify.extension.toml` is automatically generated in your app's extension directory. You can find your extension configuration in `extensions//shopify.extensions.toml`. | Property name | Description | | - | - | | `payment_session_url` required | The URL that receives payment and order details from the checkout. | | `refund_session_url` required | The URL that refund session requests are sent to. | | `capture_session_url` required | The URL that capture session requests are sent to. This is only used if your payments app supports merchant manual capture. | | `void_session_url` required | The URL that void session requests are sent to. This is only used if your payments app supports merchant manual capture or void payments. | | `supported_countries` required | The countries where your payments app is available. Refer to the [list of ISO 3166 (alpha-2) country codes](https://www.iso.org/iso-3166-country-codes.html) where your app is available for installation by merchants. | | `supports_3ds` required | 3-D Secure support is mandated in some instances. For example, you must enable the 3-D Secure field if you plan to support payments in countries which have mandated 3-D Secure. | | `supported_payment_methods` required | The payment methods (for example, Visa) that are available with your payments app. [Learn more](https://github.com/activemerchant/payment_icons/blob/master/db/payment_icons.yml). | | `supports_installments` required | Enables installments | | `supports_deferred_payments` required | Enables deferred payments | | `merchant_label` optional | The name for your payment provider extension. This name is displayed to merchants in the Shopify admin when they search for payment methods to add to their store. Limited to 50 characters. | | `buyer_label` optional | The name of the method. Your checkout name can be the same as your merchant admin name or it can be customized for customers. This name is displayed with the payment methods that you support in the customer checkout. After a checkout name has been set, translations should be provided for localization. | | `test_mode_available` optional | Enables merchants using your payments app to test their setup by simulating transactions. To test your app on a development store, your payment provider in the Shopify admin must be set to test mode. | | `api_version` optional | The Payments Apps GraphQL API version used by the payment provider app to receive requests from Shopify. You must use the same API version for sending GraphQL requests. You must not use unstable in production. API versions are updated in accordance with Shopify's general [API versioning timelines](https://shopify.dev/docs/api/usage/versioning). | | `multiple_capture` optional, closed beta | Enables merchants using your payment provider app to partially capture an authorized payment multiple times up to the full authorization amount. This is used only if your payments app supports merchant manual capture. | As you can see, the above properties are identical to what exists for the [Offsite payments app extension](https://shopify.dev/docs/apps/build/payments/offsite/use-the-cli); however, there are two additional configuration items that are worth highlighting below. ### UI Extension This is where you link the checkout extension you previously built with your payment extension. | Property name | Description | | - | - | | `ui_extension_handle` required | The UI extension that will be used to render your payments app in checkout. This value can only be a UI extension linked to this specific payments app. | ### UI Extension Field Definitions Specify the fields in your UI extensions to ensure the payment method validates the correct data sent from the front end. | Property name | Description | | - | - | | `checkout_payment_method_fields` required | The fields your payments app will accept from buyers in checkout (for example, installment details, payment plan). Each field is composed of a key name, as well as the data type, that restricts the input the buyer can provider. | ### UI Extension Field Definitions Example ```text [[extensions.checkout_payment_method_fields]] key = "bank_name" type = "string" required = true [[extensions.checkout_payment_method_fields]] key = "account_number" type = "string" required = false ``` The above field definitions would be defined for something that looks like this: ![The checkout view of a custom payment method payments app](https://cdn.shopify.com/shopifycloud/shopify-dev/production/assets/assets/apps/payments/custom-payment-method-payments-app-example-CC8dFxID.png) ## Set up your payments app ### Disable embedding Shopify apps are embedded by default, but payments apps are an exception to this, because they don't need to render anything in Shopify admin. In `shopify.app.toml`, update the `embedded` and set it to false. ### Configure basic app settings In `shopify.app.toml`, update the `name` and `client_id` to match the information about the app that you manually created. You can find the `client_id` in the **Client credentials** section of you app's overview page in the [Partner Dashboard](https://partners.shopify.com/apps/). ### Push the configuration changes to your app and start your server In a terminal, run the following commands to push the configuration changes to your app: 1. Install the packages required to run the payments app: ## Terminal ```bash npm install ``` ```bash yarn install ``` ```bash pnpm install ``` 2. Deploy your app to update the config, which is defined in `shopify.app.toml`: ## Terminal ```bash shopify app deploy ``` ### Start your development server To run the app locally, start your development server: 1. ## Terminal ```bash shopify app dev ``` Info You might be prompted to log in to your Partner account. In your terminal, select your development store. You can use the generated URL to test your payments app by using it in your [payments app configuration](#configure-your-payments-extension). If you want a consistent tunnel URL, then you can use the `--tunnel-url` flag with your own tunnel when starting your server. 2. Press `p` to open the app in your browser. This brings you to your development store's admin, where you can install your payments app. ## Explore payment sessions In this step, you'll explore the flows that an app needs to implement to process a payment. In the app template, the endpoint that handles start payment session requests is predefined, and will automatically resolve or reject the payment by calling the Payments Apps API, based on the customer's name. Note that this behavior is exclusively for testing and should be replaced with your own payment processing logic in a production app. ### Start the payment session When a customer selects your payment provider, Shopify sends an HTTP `POST` request the payment session URL for the app. The request contains information about the customer and the order. To learn more about the request body and header, refer to the [Offsite payment request reference](https://shopify.dev/apps/payments/request-reference#offsite-payment-request-reference). When the `POST` request is received, the payments app returns an HTTP `2xx` response with a `redirect_url` in the body. The `redirect_url` should be less than `8192 bytes` in length. This response and parameter are required for the payment session creation to be successful. If the request fails, then it's retried several times. If the request still fails, then the customer needs to retry their payment through Shopify Checkout. If there's an error on the payments app's side, then return an appropriate error status code instead. *** You configure the payment session URL for your app as part of the [app extension configuration](#configure-your-payments-extension). Note: It isn't necessary to specify a redirect URL unless additional information is required. You may proceed to resolve or reject the payment after acknowledging the start of the payment session, if appropriate. ## /app/routes/app.payment\_session.jsx ```jsx import { createPaymentSession } from "~/payments.repository"; /** * Saves and starts a payment session. * Redirects back to shop if payment session was created. */ export const action = async ({ request }) => { const requestBody = await request.json(); const shopDomain = request.headers.get("shopify-shop-domain"); const paymentSession = await createPaymentSession(createParams(requestBody, shopDomain)); if (!paymentSession) throw new Response("A PaymentSession couldn't be created.", { status: 500 }); return { "redirect_url": buildRedirectUrl(request, paymentSession.id) }; } const createParams = ({id, gid, group, amount, currency, test, kind, customer, payment_method, proposed_at, cancel_url}, shopDomain) => ( { id, gid, group, amount, currency, test, kind, customer, paymentMethod: payment_method, proposedAt: proposed_at, cancelUrl: cancel_url, shop: shopDomain } ) const buildRedirectUrl = (request, id) => { return `${request.url.slice(0, request.url.lastIndexOf("/"))}/payment_simulator/${id}` } ``` ### Sample payment session payload The metadata we are passing through payment session as part of the checkout UI extension would be contained within the `payment_method` request params under attributes: ## Example payment method request parameters ```json "payment_method": { "type": "custom-onsite", "data": { "attributes": [ { "key": "payment_plan", "value": "pay-in-full" } ], "cancel_url": "https://my-test-shop.com/1/checkouts/4c94d6f5b93f726a82dadfe45cdde432" } }, ``` ## Example Payload ```json { "id": "u0nwmSrNntjIWozmNslK5Tlq", "gid": "gid://shopify/PaymentSession/u0nwmSrNntjIWozmNslK5Tlq", "group": "rZNvy+1jH6Z+BcPqA5U5BSIcnUavBha3C63xBalm+xE=", "session_id": "4B2dxmle3vGgimS4deUX3+2PgLF2+/0ZWnNsNSZcgdU=", "amount": "123.00", "currency": "CAD", "test": false, "merchant_locale": "en", "payment_method": { "type": "custom-onsite", "data": { "attributes": [ { "key": "payment_plan", "value": "pay-in-full" } ], "cancel_url": "https://my-test-shop.com/1/checkouts/4c94d6f5b93f726a82dadfe45cdde432" } }, "proposed_at": "2020-07-13T00:00:00Z", "customer": { "email": "buyer@example.com", "phone_number": "5555555555", "locale": "fr", "billing_address": { "given_name": "Alice", "family_name": "Smith", "line1": "123 Street", "line2": "Suite B", "city": "Montreal", "postal_code": "H2Z 0B3", "province": "Quebec", "country_code": "CA", "phone_number": "5555555555", "company": "" }, "shipping_address": { "given_name": "Alice", "family_name": "Smith", "line1": "123 Street", "line2": "Suite B", "city": "Montreal", "postal_code": "H2Z 0B3", "province": "Quebec", "country_code": "CA", "phone_number": "5555555555", "company": "" } }, "kind": "sale" } ``` ### Sample payment session payload with localized fields For certain countries that require additional fields on orders, `localized_fields` are included in the payload inside the `transaction_metadata` object. In the case of Brazil, `localized_fields` contains the CPF value. As a result of this, payment app developers won't need to manually add a CPF field to the payments app. **Note**: Localized fields are available starting from API version `2024-07` ## Example payload with localized fields ```json "request_params": { "amount":"1.13", "app_method":"offsite", "cancel_url":"https://.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing", "currency":"CAD", "customer":{ "billing_address":{ "city":"Toronto", "country_code":"CA", "family_name":"smith", "line1":"CN Tower", "postal_code":"M5V 3L9", "province":"Ontario" }, "email":"someemail@example.com", "locale":"en-CA", "shipping_address":{ "city":"Toronto", "country_code":"CA", "family_name":"smith", "line1":"CN Tower", "postal_code":"M5V 3L9", "province":"Ontario" }, }, "fx_reconciliation":{ "currency":"CAD" }, "gid": "gid://shopify/PaymentSession/reItEndH7tKB4sGkjronhdEgv", "group": "LWXrK8B1pn0h+qX9VYtVjzRK/bTKAvXzZZqrnwZ0nnA=", "session_id": "4B2dxmle3vGgimS4deUX3+2PgLF2+/0ZWnNsNSZcgdU=", "id":"reItEndH7tKB4sGkjronhdEgv", "kind":"authorization", "merchant_locale":"en-CA", "payment_method":{ "data":{ "cancel_url":"https://.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing" }, "attributes":"[{\"key\":\"payment_plan\",\"value\":\"pay-in-full\"}]", "type":"custom_onsite" }, "payment_session":{ "amount":"1.13", "cancel_url":"https://.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing", "currency":"CAD", "customer":{ "billing_address":{ "city":"Toronto", "country_code":"CA", "family_name":"smith", "line1":"CN Tower", "postal_code":"M5V 3L9", "province":"Ontario" }, "email":"someemail@example.com", "locale":"en-CA", "shipping_address":{ "city":"Toronto", "country_code":"CA", "family_name":"smith", "line1":"CN Tower", "postal_code":"M5V 3L9", "province":"Ontario" } }, "transaction_metadata": { "localized_fields":[ { "key": "shipping", "country_code": "BR", "value": "06305371008" } ], }, "fx_reconciliation":{ "currency":"CAD" }, "gid": "gid://shopify/PaymentSession/reItEndH7tKB4sGkjronhdEgv", "group": "LWXrK8B1pn0h+qX9VYtVjzRK/bTKAvXzZZqrnwZ0nnA=", "session_id": "4B2dxmle3vGgimS4deUX3+2PgLF2+/0ZWnNsNSZcgdU=", "id":"reItEndH7tKB4sGkjronhdEgv", "kind":"authorization", "merchant_locale":"en-CA", "payment_method":{ "data":{ "cancel_url":"https://.myshopify.com/checkouts/c/157449dddf0e8eb0c1206ff66eba4b0f/processing" }, "attributes":"[{\"key\":\"payment_plan\",\"value\":\"pay-in-full\"}]", "type":"custom_onsite" }, "proposed_at":"2023-06-23T17:03:36Z", "test":false }, "proposed_at":"2023-06-23T17:03:36Z", "test":false } ``` ### Resolve a payment The payments app uses the `paymentSessionResolve` mutation after the customer has successfully gone through the payment process to complete the payment. The `id` argument corresponds to the global identifier (`gid`) of the payment. *** In the referenced code, `this.resolveMutation` corresponds to the `paymentSessionResolve` mutation. [![](https://shopify.dev/images/logos/GraphQL.svg)![](https://shopify.dev/images/logos/GraphQL-dark.svg)](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionResolve) [payment​Session​Resolve](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionResolve) ## /app/payments-apps.graphql.js ```javascript import schema from "./payments-apps.schema"; import { updatePaymentSessionStatus, updateRefundSessionStatus, updateCaptureSessionStatus, updateVoidSessionStatus, RESOLVE, REJECT, PENDING } from "./payments.repository"; /** * Client to interface with the Payments Apps GraphQL API. * * paymentsAppConfigure: Configure the payments app with the provided variables. * paymentSessionResolve: Resolves the given payment session. * paymentSessionReject: Rejects the given payment session. * refundSessionResolve: Resolves the given refund session. * refundSessionReject: Rejects the given refund session. */ export default class PaymentsAppsClient { constructor(shop, accessToken, type) { this.shop = shop; this.type = type || PAYMENT; // default this.accessToken = accessToken; this.resolveMutation = ""; this.rejectMutation = ""; this.pendingMutation = ""; this.dependencyInjector(type); } /** * Generic session resolution function * @param {*} session the session to resolve upon * @returns the response body from the Shopify Payments Apps API ``` ### Reject a payment The payments app should reject a payment if the customer can't complete a payment with the provider. The rejected payment tells Shopify that the checkout process will be halted. For example, if you don't want to process a high-risk payment, then you can reject the payment using the `paymentSessionReject` mutation. Rejecting a payment is final. You can't call other actions on a payment after it has been rejected. The payments app should retry a failed user attempt and complete the payment before calling `paymentSessionReject`. For example, if any of the following conditions are met, then you don't need to reject the payment: * The user doesn't interact with your payments app. * The user cancels the payment. * The user needs to retry the payment because of specific errors, such as the user entering the wrong CVV. *** In the referenced code, `this.rejectMutation` corresponds to the `paymentSessionReject` mutation. [![](https://shopify.dev/images/logos/GraphQL.svg)![](https://shopify.dev/images/logos/GraphQL-dark.svg)](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionReject) [payment​Session​Reject](https://shopify.dev/docs/api/payments-apps/latest/mutations/paymentSessionReject) ## /app/payments-apps.graphql.js ```javascript import schema from "./payments-apps.schema"; import { updatePaymentSessionStatus, updateRefundSessionStatus, updateCaptureSessionStatus, updateVoidSessionStatus, RESOLVE, REJECT, PENDING } from "./payments.repository"; /** * Client to interface with the Payments Apps GraphQL API. * * paymentsAppConfigure: Configure the payments app with the provided variables. * paymentSessionResolve: Resolves the given payment session. * paymentSessionReject: Rejects the given payment session. * refundSessionResolve: Resolves the given refund session. * refundSessionReject: Rejects the given refund session. */ export default class PaymentsAppsClient { constructor(shop, accessToken, type) { this.shop = shop; this.type = type || PAYMENT; // default this.accessToken = accessToken; this.resolveMutation = ""; this.rejectMutation = ""; this.pendingMutation = ""; this.dependencyInjector(type); } /** * Generic session resolution function * @param {*} session the session to resolve upon * @returns the response body from the Shopify Payments Apps API ``` ### Cancel the payment If a customer wants to cancel a payment on your provider page, then they are redirected to the merchant's website or store by using the `cancel_url`. The `cancel_url` is sent to your payments app in the payment request request-body that was sent from Shopify. Don't use the `paymentSessionReject` mutation to cancel the payment; otherwise, the customer will be unable to pay again with your provider. ## /app/routes/app.payment\_simulator.$paymentId.jsx ```jsx import { Button, Card, FooterHelp, FormLayout, Layout, Page, Text, Select, BlockStack, Link, Banner, } from "@shopify/polaris"; import { useEffect, useState } from "react"; import { Form, useLoaderData, useActionData, } from "@remix-run/react"; import { json, redirect } from "@remix-run/node"; import { sessionStorage } from "../shopify.server"; import { getPaymentSession, RESOLVE, REJECT, PENDING } from "~/payments.repository"; import PaymentsAppsClient, { PAYMENT } from "~/payments-apps.graphql"; /** * Loads the payment session being simulated. */ export const loader = async ({ params: { paymentId } }) => { const paymentSession = await getPaymentSession(paymentId); return json({ paymentSession }); } /** * Completes a payment session based on the simulator's form. */ export const action = async ({ request, params: { paymentId } }) => { const formData = await request.formData(); const resolution = formData.get("resolution"); const paymentSession = await getPaymentSession(paymentId); const session = (await sessionStorage.findSessionsByShop(paymentSession.shop))[0]; const client = new PaymentsAppsClient(session.shop, session.accessToken, PAYMENT); let response; switch(resolution) { case RESOLVE: response = await client.resolveSession(paymentSession); break; case REJECT: response = await client.rejectSession(paymentSession); break; case PENDING: response = await client.pendSession(paymentSession); break; } const userErrors = response.userErrors; if (userErrors?.length > 0) return json({ errors: userErrors }); return redirect(response.paymentSession.nextAction.context.redirectUrl); } export default function PaymentSimulator() { const action = useActionData(); const { paymentSession } = useLoaderData(); const [resolution, setResolution] = useState('resolve'); const [errors, setErrors] = useState([]); useEffect(() => { if (action?.errors.length > 0) setErrors(action.errors); }, [action]); const errorBanner = () => ( errors.length > 0 && ( { setErrors([]) }} > { errors.map(({message}, idx) => ( {message} )) } ) ) const resolutionOptions = [ {value: RESOLVE, label: 'Resolve'}, {value: REJECT, label: 'Reject'}, {value: PENDING, label: 'Pending'} ]; const cancelUrl = paymentSession.cancelUrl; return ( {errorBanner()}
setResolution(change)} value={resolution} />
Learn more about payment sessions
); } ``` ### Process next action Upon receiving the response from either the `paymentSessionResolve`, `paymentSessionReject`, or `paymentSessionPending` mutations, the next action that the payments app performs is specified under `nextAction`. The `nextAction` will either be `nil` or contain two fields. In the case where it is `nil`, no next action is expected of the payments app. Otherwise, the two fields are as follows: * `action`: A `PaymentSessionNextActionAction` enum that specifies the type of the action the app must perform. * `context`: A union type requiring inline fragments to access data on the underlying type. Takes a type of `PaymentSessionActionsRedirect`. [![](https://shopify.dev/images/logos/GraphQL.svg)![](https://shopify.dev/images/logos/GraphQL-dark.svg)](https://shopify.dev/docs/api/payments-apps/latest/objects/PaymentSessionNextAction) [next​Action](https://shopify.dev/docs/api/payments-apps/latest/objects/PaymentSessionNextAction) [![](https://shopify.dev/images/logos/GraphQL.svg)![](https://shopify.dev/images/logos/GraphQL-dark.svg)](https://shopify.dev/docs/api/payments-apps/latest/enums/PaymentSessionNextActionAction) [Payment​Session​Next​Action​Action](https://shopify.dev/docs/api/payments-apps/latest/enums/PaymentSessionNextActionAction) [![](https://shopify.dev/images/logos/GraphQL.svg)![](https://shopify.dev/images/logos/GraphQL-dark.svg)](https://shopify.dev/docs/api/payments-apps/latest/objects/PaymentSessionActionsRedirect) [Payment​Session​Actions​Redirect](https://shopify.dev/docs/api/payments-apps/latest/objects/PaymentSessionActionsRedirect) ## /app/routes/app.payment\_simulator.$paymentId.jsx ```jsx import { Button, Card, FooterHelp, FormLayout, Layout, Page, Text, Select, BlockStack, Link, Banner, } from "@shopify/polaris"; import { useEffect, useState } from "react"; import { Form, useLoaderData, useActionData, } from "@remix-run/react"; import { json, redirect } from "@remix-run/node"; import { sessionStorage } from "../shopify.server"; import { getPaymentSession, RESOLVE, REJECT, PENDING } from "~/payments.repository"; import PaymentsAppsClient, { PAYMENT } from "~/payments-apps.graphql"; /** * Loads the payment session being simulated. */ export const loader = async ({ params: { paymentId } }) => { const paymentSession = await getPaymentSession(paymentId); return json({ paymentSession }); } /** * Completes a payment session based on the simulator's form. */ export const action = async ({ request, params: { paymentId } }) => { const formData = await request.formData(); const resolution = formData.get("resolution"); const paymentSession = await getPaymentSession(paymentId); const session = (await sessionStorage.findSessionsByShop(paymentSession.shop))[0]; const client = new PaymentsAppsClient(session.shop, session.accessToken, PAYMENT); let response; switch(resolution) { case RESOLVE: response = await client.resolveSession(paymentSession); break; case REJECT: response = await client.rejectSession(paymentSession); break; case PENDING: response = await client.pendSession(paymentSession); break; } const userErrors = response.userErrors; if (userErrors?.length > 0) return json({ errors: userErrors }); return redirect(response.paymentSession.nextAction.context.redirectUrl); } export default function PaymentSimulator() { const action = useActionData(); const { paymentSession } = useLoaderData(); const [resolution, setResolution] = useState('resolve'); const [errors, setErrors] = useState([]); useEffect(() => { if (action?.errors.length > 0) setErrors(action.errors); }, [action]); const errorBanner = () => ( errors.length > 0 && ( { setErrors([]) }} > { errors.map(({message}, idx) => ( {message} )) } ) ) const resolutionOptions = [ {value: RESOLVE, label: 'Resolve'}, {value: REJECT, label: 'Reject'}, {value: PENDING, label: 'Pending'} ]; const cancelUrl = paymentSession.cancelUrl; return ( {errorBanner()}