Shopify Functions

Hi folks! I am trying to follow this documentation to set up discount using shopify functions

https://shopify.dev/docs/apps/discounts/experience/getting-started

It works well for me untill I react to the last step where I am trying to create a UI

https://shopify.dev/docs/apps/discounts/experience/ui

I am getting and error in console that appears to be in new.jsx file that says “No i18n was provided. Your application must be wrapped in an component

Any idea what might be the issue? this is my new.jsx file

import { useParams } from "react-router-dom";
import { useForm, useField } from "@shopify/react-form";
import { CurrencyCode } from "@shopify/react-i18n";
import { Redirect } from "@shopify/app-bridge/actions";
import { useAppBridge } from "@shopify/app-bridge-react";
import enTranslations from '@shopify/polaris/locales/en.json';
import {AppProvider} from '@shopify/polaris';

import {
    ActiveDatesCard,
    CombinationCard,
    DiscountClass,
    DiscountMethod,
    MethodCard,
    DiscountStatus,
    RequirementType,
    SummaryCard,
    UsageLimitsCard,
    onBreadcrumbAction,
} from "@shopify/discount-app-components";
import {
    Banner,
    Card,
    Layout,
    Page,
    TextField,
    Stack,
    PageActions,
} from "@shopify/polaris";
import { data } from "@shopify/app-bridge/actions/Modal";
import { useAuthenticatedFetch } from "../../../hooks";

const todaysDate = new Date();
// Metafield that will be used for storing function configuration
const METAFIELD_NAMESPACE = "$app:s-functions-discount";
const METAFIELD_CONFIGURATION_KEY = "s-function-configuration";

export default function VolumeNew() {
    // Read the function ID from the URL
    const { functionId } = useParams();

    const app = useAppBridge();
    const redirect = Redirect.create(app);
    const currencyCode = CurrencyCode.Cad;
    const authenticatedFetch = useAuthenticatedFetch();

    // Define base discount form fields
    const {
        fields: {
            discountTitle,
            discountCode,
            discountMethod,
            combinesWith,
            requirementType,
            requirementSubtotal,
            requirementQuantity,
            usageTotalLimit,
            usageOncePerCustomer,
            startDate,
            endDate,
            configuration,
        },
        submit,
        submitting,
        dirty,
        reset,
        submitErrors,
        makeClean,
    } = useForm({
        fields: {
            discountTitle: useField(""),
            discountMethod: useField(DiscountMethod.Code),
            discountCode: useField(""),
            combinesWith: useField({
                orderDiscounts: false,
                productDiscounts: false,
                shippingDiscounts: false,
            }),
            requirementType: useField(RequirementType.None),
            requirementSubtotal: useField("0"),
            requirementQuantity: useField("0"),
            usageTotalLimit: useField(null),
            usageOncePerCustomer: useField(false),
            startDate: useField(todaysDate),
            endDate: useField(null),
            configuration: { // Add quantity and percentage configuration to form data
                quantity: useField('1'),
                percentage: useField('0'),
            }
        },
        onSubmit: async (form) => {
            // Create the discount using the added express endpoints
            const discount = {
                functionId,
                combinesWith: form.combinesWith,
                startsAt: form.startDate,
                endsAt: form.endDate,
                metafields: [
                    {
                        namespace: METAFIELD_NAMESPACE,
                        key: METAFIELD_CONFIGURATION_KEY,
                        type: "json",
                        value: JSON.stringify({ // Populate metafield from form data
                            quantity: parseInt(form.configuration.quantity),
                            percentage: parseFloat(form.configuration.percentage),
                        }),
                    },
                ],
            };

            let response;
            if (form.discountMethod === DiscountMethod.Automatic) {
                response = await authenticatedFetch("/api/discounts/automatic", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        discount: {
                            ...discount,
                            title: form.discountTitle,
                        },
                    }),
                });
            } else {
                response = await authenticatedFetch("/api/discounts/code", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        discount: {
                            ...discount,
                            title: form.discountCode,
                            code: form.discountCode,
                        },
                    }),
                });
            }

            const data = (await response.json()).data;
            const remoteErrors = data.discountCreate.userErrors;
            if (remoteErrors.length > 0) {
                return { status: "fail", errors: remoteErrors };
            }

            redirect.dispatch(Redirect.Action.ADMIN_SECTION, {
                name: Redirect.ResourceType.Discount,
            });
            return { status: "success" };
        },
    });

    const errorBanner =
        submitErrors.length > 0 ? (
            <Layout.Section>
                <Banner status="critical">
                    <p>There were some issues with your form submission:</p>
                    <ul>
                        {submitErrors.map(({ message, field }, index) => {
                            return (
                                <li key={`${message}${index}`}>
                                    {field.join(".")} {message}
                                </li>
                            );
                        })}
                    </ul>
                </Banner>
            </Layout.Section>
        ) : null;

    return (
        // Render a discount form using Polaris components and the discount app components
        <Page
            title="Create volume discount"
            breadcrumbs={[
                {
                    content: "Discounts",
                    onAction: () => onBreadcrumbAction(redirect, true),
                },
            ]}
            primaryAction={{
                content: "Save",
                onAction: submit,
                disabled: !dirty,
                loading: submitting,
            }}
        >
            <Layout>
                {errorBanner}
                <Layout.Section>
                    <form onSubmit={submit}>
                        <MethodCard
                            title="Volume"
                            discountTitle={discountTitle}
                            discountClass={DiscountClass.Product}
                            discountCode={discountCode}
                            discountMethod={discountMethod}
                        />
                        <Card title="Volume">
                            <Card.Section>
                                <Stack>
                                    <TextField label="Minimum quantity" {...configuration.quantity} />
                                    <TextField label="Discount percentage" {...configuration.percentage} suffix="%" />
                                </Stack>
                            </Card.Section>
                        </Card>
                        {discountMethod.value === DiscountMethod.Code && (
                            <UsageLimitsCard
                                totalUsageLimit={usageTotalLimit}
                                oncePerCustomer={usageOncePerCustomer}
                            />
                        )}
                        <CombinationCard
                            combinableDiscountTypes={combinesWith}
                            discountClass={DiscountClass.Product}
                            discountDescriptor={
                                discountMethod.value === DiscountMethod.Automatic
                                    ? discountTitle.value
                                    : discountCode.value
                            }
                        />
                        <ActiveDatesCard
                            startDate={startDate}
                            endDate={endDate}
                            timezoneAbbreviation="EST"
                        />
                    </form>
                </Layout.Section>
                <Layout.Section secondary>
                    <SummaryCard
                        header={{
                            discountMethod: discountMethod.value,
                            discountDescriptor:
                                discountMethod.value === DiscountMethod.Automatic
                                    ? discountTitle.value
                                    : discountCode.value,
                            appDiscountType: "Volume",
                            isEditing: false,
                        }}
                        performance={{
                            status: DiscountStatus.Scheduled,
                            usageCount: 0,
                        }}
                        minimumRequirements={{
                            requirementType: requirementType.value,
                            subtotal: requirementSubtotal.value,
                            quantity: requirementQuantity.value,
                            currencyCode: currencyCode,
                        }}
                        usageLimits={{
                            oncePerCustomer: usageOncePerCustomer.value,
                            totalUsageLimit: usageTotalLimit.value,
                        }}
                        activeDates={{
                            startDate: startDate.value,
                            endDate: endDate.value,
                        }}
                    />
                </Layout.Section>
                <Layout.Section>
                    <PageActions
                        primaryAction={{
                            content: "Save discount",
                            onAction: submit,
                            disabled: !dirty,
                            loading: submitting,
                        }}
                        secondaryActions={[
                            {
                                content: "Discard",
                                onAction: () => onBreadcrumbAction(redirect, true),
                            },
                        ]}
                    />
                </Layout.Section>
            </Layout>
        </Page>
    );
}

Hi! It probably refers to your component inside the App.jsx file. You need to wrap that with the or AppBridgeProvider depending on what you need.

Hope this is helpful for someone else - I ran into this problem and it was because I was not using the from @Shopify_77 /discount-app-components to wrap the page.

According to the readme, you should add the AppProvider component from that library inside of the PolarisAppComponent, and pass a locale and ianaTimezone props to it:

import {Page, AppProvider as PolarisAppProvider} from '@shopify/polaris';
import {Provider as AppBridgeProvider} from '@shopify/app-bridge-react';
import {AppProvider} from '@shopify/discount-app-components';

// See [Polaris AppProvider documentation](https://github.com/Shopify/polaris/blob/main/polaris-react/src/components/AppProvider/README.md#using-translations) for more details on using Polaris translations
import enPolarisTranslations from '@shopify/polaris/locales/en.json';

// Import polaris styles
import "@shopify/polaris/build/esm/styles.css";

// Import this discount-app-components styles
import "@shopify/discount-app-components/build/esm/styles.css";

export default function App() {

  ...

  return (
    
  );
}

import { useState } from “react”;
import { json } from “@remix-run/node”;
import { Link, Outlet, useLoaderData, useRouteError } from “@remix-run/react”;
import { default as pkg } from “@shopify/app-bridge-react”;
import “@shopify/polaris/build/esm/styles.css”;
import { DiscountProvider } from “../components/providers/DiscountProvider”;
import { boundary } from “@shopify/shopify-app-remix/server”;
import { AppProvider } from “@shopify/polaris”;
import { authenticate } from “../shopify.server”;

export async function loader({ request }) {
await authenticate.admin(request);

const url = new URL(request.url);

return json({
apiKey: process.env.SHOPIFY_API_KEY,
host: url.searchParams.get(“host”),
});
}

export default function App() {
const { apiKey, host } = useLoaderData();
const [config] = useState({ host, apiKey });

return (

<pkg.Provider config={config}>

Home Additional page ); }

// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
return boundary.error(useRouteError());
}

export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};

Can anyone here tell what is wrong with my code above is my App.jsx and i’m getting the error below. Just followed the shopify documentation:
https://shopify.dev/docs/apps/selling-strategies/discounts/experience/ui#step-6-create-and-test-your-discount

import { useState } from “react”;
import { json } from “@remix-run/node”;
import { Link, Outlet, useLoaderData, useRouteError } from “@remix-run/react”;
import { default as pkg } from “@shopify/app-bridge-react”;
import “@shopify/polaris/build/esm/styles.css”;
import { DiscountProvider } from “../components/providers/DiscountProvider”;
import { boundary } from “@shopify/shopify-app-remix/server”;
import { AppProvider } from “@shopify/polaris”;
import { authenticate } from “../shopify.server”;

export async function loader({ request }) {
await authenticate.admin(request);

const url = new URL(request.url);

return json({
apiKey: process.env.SHOPIFY_API_KEY,
host: url.searchParams.get(“host”),
});
}

export default function App() {
const { apiKey, host } = useLoaderData();
const [config] = useState({ host, apiKey });

return (

<pkg.Provider config={config}>

Home Additional page ); }

// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
return boundary.error(useRouteError());
}

export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};

Can anyone here tell what is wrong with my code above is my App.jsx and i’m getting the error below. Just followed the shopify documentation:
https://shopify.dev/docs/apps/selling-strategies/discounts/experience/ui#step-6-create-and-test-your…