Shopify Functions

Shopify Functions

Sha_Hassan
Shopify Partner
12 0 4

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 <AppProvider> 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>
    );
}
Replies 4 (4)

nadinethery
Shopify Partner
19 1 6

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

itsanolive
Shopify Partner
21 0 10

Hope this is helpful for someone else - I ran into this problem and it was because I was not using the <AppProvider> from @Shopify/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 (
    <AppBridgeProvider config={/* pass your app bridge config here */}>
      <PolarisAppProvider i18n={enPolarisTranslations}>
        <AppProvider locale="en-US" ianaTimezone="America/Los_Angeles">
          <Page title="Example app">
            {/* Add your discount components here */}
          </Page>
        </AppProvider>
      </PolarisAppProvider>
    </AppBridgeProvider>
  );
}

 

muneebtanveer96
Shopify Partner
2 0 0
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 (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <pkg.Provider config={config}>
        <DiscountProvider>
          <ui-nav-menu>
            <Link to="/app" rel="home">
              Home
            </Link>
            <Link to="/app/additional">Additional page</Link>
          </ui-nav-menu>
          <Outlet />
        </DiscountProvider>
      </pkg.Provider>
    </AppProvider>
  );
}

// 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:
 
muneebtanveer96_0-1709321812849.png

 

muneebtanveer96
Shopify Partner
2 0 0
import { useState } from "react";
import { json } from "@remix-run/node";
import { LinkOutletuseLoaderDatauseRouteError } 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 { apiKeyhost } = useLoaderData();
  const [config= useState({ hostapiKey });
 
  return (
    <AppProvider isEmbeddedApp apiKey={apiKey}>
      <pkg.Provider config={config}>
        <DiscountProvider>
          <ui-nav-menu>
            <Link to="/app" rel="home">
              Home
            </Link>
            <Link to="/app/additional">Additional page</Link>
          </ui-nav-menu>
          <Outlet />
        </DiscountProvider>
      </pkg.Provider>
    </AppProvider>
  );
}

 

// 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:
 
muneebtanveer96_1-1709321958515.png