Discussing Shopify Functions development, deployment, and usage in Shopify apps.
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> ); }
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.
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.
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>
);
}