├── .git-blame-ignore-revs ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── cla.yml │ └── merge_patch_dependencies.yml ├── .gitignore ├── App.jsx ├── LICENSE.md ├── README.md ├── Routes.jsx ├── assets ├── empty-state.svg ├── home-trophy.png └── index.js ├── components ├── ProductsCard.jsx ├── index.js └── providers │ ├── AppBridgeProvider.jsx │ ├── PolarisProvider.jsx │ ├── QueryProvider.jsx │ └── index.js ├── dev_embed.js ├── hooks ├── index.js ├── useAppQuery.js └── useAuthenticatedFetch.js ├── index.html ├── index.jsx ├── package.json ├── pages ├── ExitIframe.jsx ├── NotFound.jsx ├── index.jsx └── pagename.jsx ├── shopify.web.toml ├── translation.yml └── vite.config.js /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | ## 2022-06-21: Run prettier 2 | fa34bd26183ddc22701e2982288b33e6c8020f9e 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@shopify.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct/ 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Issue summary 2 | 3 | Write a short description of the issue here ↓ 4 | 5 | ## Expected behavior 6 | 7 | What do you think should happen? 8 | 9 | ## Actual behavior 10 | 11 | What actually happens? 12 | 13 | Tip: include an error message (in a `
` tag) if your issue is related to an error 14 | 15 | ## Steps to reproduce the problem 16 | 17 | 1. 1. 1. 18 | 19 | ## Reduced test case 20 | 21 | The best way to get your bug fixed is to provide a reduced test case. 22 | 23 | ## Specifications 24 | 25 | - Browser: 26 | - Device: 27 | - Operating System: 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### WHY are these changes introduced? 10 | 11 | Fixes #0000 12 | 13 | 16 | 17 | ### WHAT is this pull request doing? 18 | 19 | 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: "npm" 5 | # Look for `package.json` and `lock` files in the `root` directory 6 | directory: "/" 7 | # Check the npm registry for updates every day (weekdays) 8 | schedule: 9 | interval: "daily" 10 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: Contributor License Agreement (CLA) 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize] 6 | issue_comment: 7 | types: [created] 8 | 9 | jobs: 10 | cla: 11 | runs-on: ubuntu-latest 12 | if: | 13 | (github.event.issue.pull_request 14 | && !github.event.issue.pull_request.merged_at 15 | && contains(github.event.comment.body, 'signed') 16 | ) 17 | || (github.event.pull_request && !github.event.pull_request.merged) 18 | steps: 19 | - uses: Shopify/shopify-cla-action@v1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | cla-token: ${{ secrets.CLA_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/merge_patch_dependencies.yml: -------------------------------------------------------------------------------- 1 | on: pull_request_target 2 | 3 | name: "Dependabot: auto-merge patch versions" 4 | 5 | jobs: 6 | approve-dependabot-pr: 7 | if: ${{ github.actor == 'dependabot[bot]' }} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Dependabot metadata 11 | id: dependabot-metadata 12 | uses: dependabot/fetch-metadata@v1 13 | with: 14 | github-token: "${{ secrets.GITHUB_TOKEN }}" 15 | - name: Approve and merge Dependabot PRs for patch versions 16 | if: ${{steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch'}} 17 | uses: actions/github-script@v5 18 | with: 19 | script: | 20 | await github.rest.pulls.createReview({ 21 | pull_number: context.issue.number, 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | event: 'APPROVE', 25 | }) 26 | 27 | await github.rest.pulls.merge({ 28 | merge_method: "merge", 29 | owner: repository.owner, 30 | pull_number: pullRequest.number, 31 | repo: repository.repo, 32 | }) 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Ignore Apple macOS Desktop Services Store 5 | .DS_Store 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # vite build output 12 | dist/ 13 | 14 | # Partners can use npm, yarn or pnpm with the CLI. 15 | # We ignore lock files so they don't get a package manager mis-match 16 | # Without this, they may get a warning if using a different package manager to us 17 | yarn.lock 18 | package-lock.json 19 | -------------------------------------------------------------------------------- /App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter } from "react-router-dom"; 2 | import { NavigationMenu } from "@shopify/app-bridge-react"; 3 | import Routes from "./Routes"; 4 | 5 | import { 6 | AppBridgeProvider, 7 | QueryProvider, 8 | PolarisProvider, 9 | } from "./components"; 10 | 11 | export default function App() { 12 | // Any .tsx or .jsx files in /pages will become a route 13 | // See documentation for for more info 14 | const pages = import.meta.globEager("./pages/**/!(*.test.[jt]sx)*.([jt]sx)"); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shopify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopify React Frontend App 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) 4 | 5 | This repository is the frontend for Shopify’s app starter templates. **You probably don’t want to use this repository directly**, but rather through one of the templates and the [Shopify CLI](https://github.com/Shopify/shopify-cli). 6 | 7 | ## Developer resources 8 | 9 | - [Introduction to Shopify apps](https://shopify.dev/apps/getting-started) 10 | - [App authentication](https://shopify.dev/apps/auth) 11 | - [Shopify CLI command reference](https://shopify.dev/apps/tools/cli/app) 12 | - [Shopify API Library documentation](https://github.com/Shopify/shopify-node-api/tree/main/docs) 13 | 14 | ## License 15 | 16 | This repository is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 17 | -------------------------------------------------------------------------------- /Routes.jsx: -------------------------------------------------------------------------------- 1 | import { Routes as ReactRouterRoutes, Route } from "react-router-dom"; 2 | 3 | /** 4 | * File-based routing. 5 | * @desc File-based routing that uses React Router under the hood. 6 | * To create a new route create a new .jsx file in `/pages` with a default export. 7 | * 8 | * Some examples: 9 | * * `/pages/index.jsx` matches `/` 10 | * * `/pages/blog/[id].jsx` matches `/blog/123` 11 | * * `/pages/[...catchAll].jsx` matches any URL not explicitly matched 12 | * 13 | * @param {object} pages value of import.meta.globEager(). See https://vitejs.dev/guide/features.html#glob-import 14 | * 15 | * @return {Routes} `` from React Router, with a `` for each file in `pages` 16 | */ 17 | export default function Routes({ pages }) { 18 | const routes = useRoutes(pages); 19 | const routeComponents = routes.map(({ path, component: Component }) => ( 20 | } /> 21 | )); 22 | 23 | const NotFound = routes.find(({ path }) => path === "/notFound").component; 24 | 25 | return ( 26 | 27 | {routeComponents} 28 | } /> 29 | 30 | ); 31 | } 32 | 33 | function useRoutes(pages) { 34 | const routes = Object.keys(pages) 35 | .map((key) => { 36 | let path = key 37 | .replace("./pages", "") 38 | .replace(/\.(t|j)sx?$/, "") 39 | /** 40 | * Replace /index with / 41 | */ 42 | .replace(/\/index$/i, "/") 43 | /** 44 | * Only lowercase the first letter. This allows the developer to use camelCase 45 | * dynamic paths while ensuring their standard routes are normalized to lowercase. 46 | */ 47 | .replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase()) 48 | /** 49 | * Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom 50 | */ 51 | .replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`); 52 | 53 | if (path.endsWith("/") && path !== "/") { 54 | path = path.substring(0, path.length - 1); 55 | } 56 | 57 | if (!pages[key].default) { 58 | console.warn(`${key} doesn't export a default React component`); 59 | } 60 | 61 | return { 62 | path, 63 | component: pages[key].default, 64 | }; 65 | }) 66 | .filter((route) => route.component); 67 | 68 | return routes; 69 | } 70 | -------------------------------------------------------------------------------- /assets/empty-state.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/home-trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devshopier/shopify-frontend-template-react/1e920312bbe4309a9190a44ef8cb37743976b377/assets/home-trophy.png -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | export { default as notFoundImage } from "./empty-state.svg"; 2 | export { default as trophyImage } from "./home-trophy.png"; 3 | -------------------------------------------------------------------------------- /components/ProductsCard.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Card, TextContainer, Text } from "@shopify/polaris"; 3 | import { Toast } from "@shopify/app-bridge-react"; 4 | import { useAppQuery, useAuthenticatedFetch } from "../hooks"; 5 | 6 | export function ProductsCard() { 7 | const emptyToastProps = { content: null }; 8 | const [isLoading, setIsLoading] = useState(true); 9 | const [toastProps, setToastProps] = useState(emptyToastProps); 10 | const fetch = useAuthenticatedFetch(); 11 | 12 | const { 13 | data, 14 | refetch: refetchProductCount, 15 | isLoading: isLoadingCount, 16 | isRefetching: isRefetchingCount, 17 | } = useAppQuery({ 18 | url: "/api/products/count", 19 | reactQueryOptions: { 20 | onSuccess: () => { 21 | setIsLoading(false); 22 | }, 23 | }, 24 | }); 25 | 26 | const toastMarkup = toastProps.content && !isRefetchingCount && ( 27 | setToastProps(emptyToastProps)} /> 28 | ); 29 | 30 | const handlePopulate = async () => { 31 | setIsLoading(true); 32 | const response = await fetch("/api/products/create"); 33 | 34 | if (response.ok) { 35 | await refetchProductCount(); 36 | setToastProps({ content: "5 products created!" }); 37 | } else { 38 | setIsLoading(false); 39 | setToastProps({ 40 | content: "There was an error creating products", 41 | error: true, 42 | }); 43 | } 44 | }; 45 | 46 | return ( 47 | <> 48 | {toastMarkup} 49 | 58 | 59 |

60 | Sample products are created with a default title and price. You can 61 | remove them at any time. 62 |

63 | 64 | TOTAL PRODUCTS 65 | 66 | {isLoadingCount ? "-" : data.count} 67 | 68 | 69 |
70 |
71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | export { ProductsCard } from "./ProductsCard"; 2 | export * from "./providers"; 3 | -------------------------------------------------------------------------------- /components/providers/AppBridgeProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import { Provider } from "@shopify/app-bridge-react"; 4 | import { Banner, Layout, Page } from "@shopify/polaris"; 5 | 6 | /** 7 | * A component to configure App Bridge. 8 | * @desc A thin wrapper around AppBridgeProvider that provides the following capabilities: 9 | * 10 | * 1. Ensures that navigating inside the app updates the host URL. 11 | * 2. Configures the App Bridge Provider, which unlocks functionality provided by the host. 12 | * 13 | * See: https://shopify.dev/apps/tools/app-bridge/getting-started/using-react 14 | */ 15 | export function AppBridgeProvider({ children }) { 16 | const location = useLocation(); 17 | const navigate = useNavigate(); 18 | const history = useMemo( 19 | () => ({ 20 | replace: (path) => { 21 | navigate(path, { replace: true }); 22 | }, 23 | }), 24 | [navigate] 25 | ); 26 | 27 | const routerConfig = useMemo( 28 | () => ({ history, location }), 29 | [history, location] 30 | ); 31 | 32 | // The host may be present initially, but later removed by navigation. 33 | // By caching this in state, we ensure that the host is never lost. 34 | // During the lifecycle of an app, these values should never be updated anyway. 35 | // Using state in this way is preferable to useMemo. 36 | // See: https://stackoverflow.com/questions/60482318/version-of-usememo-for-caching-a-value-that-will-never-change 37 | const [appBridgeConfig] = useState(() => { 38 | const host = 39 | new URLSearchParams(location.search).get("host") || 40 | window.__SHOPIFY_DEV_HOST; 41 | 42 | window.__SHOPIFY_DEV_HOST = host; 43 | 44 | return { 45 | host, 46 | apiKey: process.env.SHOPIFY_API_KEY, 47 | forceRedirect: true, 48 | }; 49 | }); 50 | 51 | if (!process.env.SHOPIFY_API_KEY || !appBridgeConfig.host) { 52 | const bannerProps = !process.env.SHOPIFY_API_KEY 53 | ? { 54 | title: "Missing Shopify API Key", 55 | children: ( 56 | <> 57 | Your app is running without the SHOPIFY_API_KEY environment 58 | variable. Please ensure that it is set when running or building 59 | your React app. 60 | 61 | ), 62 | } 63 | : { 64 | title: "Missing host query argument", 65 | children: ( 66 | <> 67 | Your app can only load if the URL has a host argument. 68 | Please ensure that it is set, or access your app using the 69 | Partners Dashboard Test your app feature 70 | 71 | ), 72 | }; 73 | 74 | return ( 75 | 76 | 77 | 78 |
79 | 80 |
81 |
82 |
83 |
84 | ); 85 | } 86 | 87 | return ( 88 | 89 | {children} 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /components/providers/PolarisProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { AppProvider } from "@shopify/polaris"; 3 | import { useNavigate } from "@shopify/app-bridge-react"; 4 | import translations from "@shopify/polaris/locales/en.json"; 5 | import "@shopify/polaris/build/esm/styles.css"; 6 | 7 | function AppBridgeLink({ url, children, external, ...rest }) { 8 | const navigate = useNavigate(); 9 | const handleClick = useCallback(() => { 10 | navigate(url); 11 | }, [url]); 12 | 13 | const IS_EXTERNAL_LINK_REGEX = /^(?:[a-z][a-z\d+.-]*:|\/\/)/; 14 | 15 | if (external || IS_EXTERNAL_LINK_REGEX.test(url)) { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | 30 | /** 31 | * Sets up the AppProvider from Polaris. 32 | * @desc PolarisProvider passes a custom link component to Polaris. 33 | * The Link component handles navigation within an embedded app. 34 | * Prefer using this vs any other method such as an anchor. 35 | * Use it by importing Link from Polaris, e.g: 36 | * 37 | * ``` 38 | * import {Link} from '@shopify/polaris' 39 | * 40 | * function MyComponent() { 41 | * return ( 42 | *
Tab 2
43 | * ) 44 | * } 45 | * ``` 46 | * 47 | * PolarisProvider also passes translations to Polaris. 48 | * 49 | */ 50 | export function PolarisProvider({ children }) { 51 | return ( 52 | 53 | {children} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/providers/QueryProvider.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | QueryClient, 3 | QueryClientProvider, 4 | QueryCache, 5 | MutationCache, 6 | } from "react-query"; 7 | 8 | /** 9 | * Sets up the QueryClientProvider from react-query. 10 | * @desc See: https://react-query.tanstack.com/reference/QueryClientProvider#_top 11 | */ 12 | export function QueryProvider({ children }) { 13 | const client = new QueryClient({ 14 | queryCache: new QueryCache(), 15 | mutationCache: new MutationCache(), 16 | }); 17 | 18 | return {children}; 19 | } 20 | -------------------------------------------------------------------------------- /components/providers/index.js: -------------------------------------------------------------------------------- 1 | export { AppBridgeProvider } from "./AppBridgeProvider"; 2 | export { QueryProvider } from "./QueryProvider"; 3 | export { PolarisProvider } from "./PolarisProvider"; 4 | -------------------------------------------------------------------------------- /dev_embed.js: -------------------------------------------------------------------------------- 1 | import RefreshRuntime from "/@react-refresh"; 2 | 3 | RefreshRuntime.injectIntoGlobalHook(window); 4 | window.$RefreshReg$ = () => {}; 5 | window.$RefreshSig$ = () => (type) => type; 6 | window.__vite_plugin_react_preamble_installed__ = true; 7 | -------------------------------------------------------------------------------- /hooks/index.js: -------------------------------------------------------------------------------- 1 | export { useAppQuery } from "./useAppQuery"; 2 | export { useAuthenticatedFetch } from "./useAuthenticatedFetch"; 3 | -------------------------------------------------------------------------------- /hooks/useAppQuery.js: -------------------------------------------------------------------------------- 1 | import { useAuthenticatedFetch } from "./useAuthenticatedFetch"; 2 | import { useMemo } from "react"; 3 | import { useQuery } from "react-query"; 4 | 5 | /** 6 | * A hook for querying your custom app data. 7 | * @desc A thin wrapper around useAuthenticatedFetch and react-query's useQuery. 8 | * 9 | * @param {Object} options - The options for your query. Accepts 3 keys: 10 | * 11 | * 1. url: The URL to query. E.g: /api/widgets/1` 12 | * 2. fetchInit: The init options for fetch. See: https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters 13 | * 3. reactQueryOptions: The options for `useQuery`. See: https://react-query.tanstack.com/reference/useQuery 14 | * 15 | * @returns Return value of useQuery. See: https://react-query.tanstack.com/reference/useQuery. 16 | */ 17 | export const useAppQuery = ({ url, fetchInit = {}, reactQueryOptions }) => { 18 | const authenticatedFetch = useAuthenticatedFetch(); 19 | const fetch = useMemo(() => { 20 | return async () => { 21 | const response = await authenticatedFetch(url, fetchInit); 22 | return response.json(); 23 | }; 24 | }, [url, JSON.stringify(fetchInit)]); 25 | 26 | return useQuery(url, fetch, { 27 | ...reactQueryOptions, 28 | refetchOnWindowFocus: false, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /hooks/useAuthenticatedFetch.js: -------------------------------------------------------------------------------- 1 | import { authenticatedFetch } from "@shopify/app-bridge/utilities"; 2 | import { useAppBridge } from "@shopify/app-bridge-react"; 3 | import { Redirect } from "@shopify/app-bridge/actions"; 4 | 5 | /** 6 | * A hook that returns an auth-aware fetch function. 7 | * @desc The returned fetch function that matches the browser's fetch API 8 | * See: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 9 | * It will provide the following functionality: 10 | * 11 | * 1. Add a `X-Shopify-Access-Token` header to the request. 12 | * 2. Check response for `X-Shopify-API-Request-Failure-Reauthorize` header. 13 | * 3. Redirect the user to the reauthorization URL if the header is present. 14 | * 15 | * @returns {Function} fetch function 16 | */ 17 | export function useAuthenticatedFetch() { 18 | const app = useAppBridge(); 19 | const fetchFunction = authenticatedFetch(app); 20 | 21 | return async (uri, options) => { 22 | const response = await fetchFunction(uri, options); 23 | checkHeadersForReauthorization(response.headers, app); 24 | return response; 25 | }; 26 | } 27 | 28 | function checkHeadersForReauthorization(headers, app) { 29 | if (headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1") { 30 | const authUrlHeader = 31 | headers.get("X-Shopify-API-Request-Failure-Reauthorize-Url") || 32 | `/api/auth`; 33 | 34 | const redirect = Redirect.create(app); 35 | redirect.dispatch( 36 | Redirect.Action.REMOTE, 37 | authUrlHeader.startsWith("/") 38 | ? `https://${window.location.host}${authUrlHeader}` 39 | : authUrlHeader 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /index.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | 3 | import App from "./App"; 4 | 5 | ReactDOM.render(, document.getElementById("app")); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-frontend-template-react", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "UNLICENSED", 6 | "scripts": { 7 | "build": "vite build", 8 | "dev": "vite", 9 | "coverage": "vitest run --coverage" 10 | }, 11 | "type": "module", 12 | "engines": { 13 | "node": ">= 12.16" 14 | }, 15 | "stylelint": { 16 | "extends": "@shopify/stylelint-polaris" 17 | }, 18 | "dependencies": { 19 | "@shopify/app-bridge": "^3.7.7", 20 | "@shopify/app-bridge-react": "^3.7.7", 21 | "@shopify/polaris": "^10.49.1", 22 | "@vitejs/plugin-react": "1.2.0", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "react-query": "^3.34.19", 26 | "react-router-dom": "^6.3.0", 27 | "vite": "^2.8.6" 28 | }, 29 | "devDependencies": { 30 | "history": "^5.3.0", 31 | "jsdom": "^19.0.0", 32 | "prettier": "^2.6.0", 33 | "vi-fetch": "^0.6.1", 34 | "@shopify/stylelint-polaris": "^12.0.0", 35 | "stylelint": "^15.6.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pages/ExitIframe.jsx: -------------------------------------------------------------------------------- 1 | import { Redirect } from "@shopify/app-bridge/actions"; 2 | import { useAppBridge, Loading } from "@shopify/app-bridge-react"; 3 | import { useEffect, useState } from "react"; 4 | import { useLocation } from "react-router-dom"; 5 | import { Banner, Layout, Page } from "@shopify/polaris"; 6 | 7 | export default function ExitIframe() { 8 | const app = useAppBridge(); 9 | const { search } = useLocation(); 10 | const [showWarning, setShowWarning] = useState(false); 11 | 12 | useEffect(() => { 13 | if (!!app && !!search) { 14 | const params = new URLSearchParams(search); 15 | const redirectUri = params.get("redirectUri"); 16 | const url = new URL(decodeURIComponent(redirectUri)); 17 | 18 | if ( 19 | [location.hostname, "admin.shopify.com"].includes(url.hostname) || 20 | url.hostname.endsWith(".myshopify.com") 21 | ) { 22 | const redirect = Redirect.create(app); 23 | redirect.dispatch( 24 | Redirect.Action.REMOTE, 25 | decodeURIComponent(redirectUri) 26 | ); 27 | } else { 28 | setShowWarning(true); 29 | } 30 | } 31 | }, [app, search, setShowWarning]); 32 | 33 | return showWarning ? ( 34 | 35 | 36 | 37 |
38 | 39 | Apps can only use /exitiframe to reach Shopify or the app itself. 40 | 41 |
42 |
43 |
44 |
45 | ) : ( 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /pages/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import { Card, EmptyState, Page } from "@shopify/polaris"; 2 | import { notFoundImage } from "../assets"; 3 | 4 | export default function NotFound() { 5 | return ( 6 | 7 | 8 | 9 | 13 |

14 | Check the URL and try again, or use the search bar to find what 15 | you need. 16 |

17 |
18 |
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Page, 4 | Layout, 5 | TextContainer, 6 | Image, 7 | Stack, 8 | Link, 9 | Text, 10 | } from "@shopify/polaris"; 11 | import { TitleBar } from "@shopify/app-bridge-react"; 12 | 13 | import { trophyImage } from "../assets"; 14 | 15 | import { ProductsCard } from "../components"; 16 | 17 | export default function HomePage() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | Nice work on building a Shopify app 🎉 34 | 35 |

36 | Your app is ready to explore! It contains everything you 37 | need to get started including the{" "} 38 | 39 | Polaris design system 40 | 41 | ,{" "} 42 | 43 | Shopify Admin API 44 | 45 | , and{" "} 46 | 50 | App Bridge 51 | {" "} 52 | UI library and components. 53 |

54 |

55 | Ready to go? Start populating your app with some sample 56 | products to view and test in your store.{" "} 57 |

58 |

59 | Learn more about building out your app in{" "} 60 | 64 | this Shopify tutorial 65 | {" "} 66 | 📚{" "} 67 |

68 |
69 |
70 | 71 |
72 | Nice work on building a Shopify app 77 |
78 |
79 |
80 |
81 |
82 | 83 | 84 | 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /pages/pagename.jsx: -------------------------------------------------------------------------------- 1 | import { Card, Page, Layout, TextContainer, Text } from "@shopify/polaris"; 2 | import { TitleBar } from "@shopify/app-bridge-react"; 3 | 4 | export default function PageName() { 5 | return ( 6 | 7 | console.log("Primary action"), 12 | }} 13 | secondaryActions={[ 14 | { 15 | content: "Secondary action", 16 | onAction: () => console.log("Secondary action"), 17 | }, 18 | ]} 19 | /> 20 | 21 | 22 | 23 | 24 | Heading 25 | 26 | 27 |

Body

28 |
29 |
30 | 31 | 32 | Heading 33 | 34 | 35 |

Body

36 |
37 |
38 |
39 | 40 | 41 | 42 | Heading 43 | 44 | 45 |

Body

46 |
47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /shopify.web.toml: -------------------------------------------------------------------------------- 1 | type="frontend" 2 | 3 | [commands] 4 | dev = "npm run dev" 5 | build = "npm run build" 6 | -------------------------------------------------------------------------------- /translation.yml: -------------------------------------------------------------------------------- 1 | source_language: en 2 | target_languages: [de, fr] 3 | non_blocking_languages: [] 4 | async_pr_mode: per_pr 5 | components: 6 | - name: merchant 7 | audience: merchant 8 | scheme: apps-and-themes 9 | owners: ["@Shopify/client-libraries-app-templates"] 10 | paths: 11 | - locales/**/{{language}}.json 12 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { dirname } from "path"; 3 | import { fileURLToPath } from "url"; 4 | import https from "https"; 5 | import react from "@vitejs/plugin-react"; 6 | 7 | if ( 8 | process.env.npm_lifecycle_event === "build" && 9 | !process.env.CI && 10 | !process.env.SHOPIFY_API_KEY 11 | ) { 12 | console.warn( 13 | "\nBuilding the frontend app without an API key. The frontend build will not run without an API key. Set the SHOPIFY_API_KEY environment variable when running the build command.\n" 14 | ); 15 | } 16 | 17 | const proxyOptions = { 18 | target: `http://127.0.0.1:${process.env.BACKEND_PORT}`, 19 | changeOrigin: false, 20 | secure: true, 21 | ws: false, 22 | }; 23 | 24 | const host = process.env.HOST 25 | ? process.env.HOST.replace(/https?:\/\//, "") 26 | : "localhost"; 27 | 28 | let hmrConfig; 29 | if (host === "localhost") { 30 | hmrConfig = { 31 | protocol: "ws", 32 | host: "localhost", 33 | port: 64999, 34 | clientPort: 64999, 35 | }; 36 | } else { 37 | hmrConfig = { 38 | protocol: "wss", 39 | host: host, 40 | port: process.env.FRONTEND_PORT, 41 | clientPort: 443, 42 | }; 43 | } 44 | 45 | export default defineConfig({ 46 | root: dirname(fileURLToPath(import.meta.url)), 47 | plugins: [react()], 48 | define: { 49 | "process.env.SHOPIFY_API_KEY": JSON.stringify(process.env.SHOPIFY_API_KEY), 50 | }, 51 | resolve: { 52 | preserveSymlinks: true, 53 | }, 54 | server: { 55 | host: "localhost", 56 | port: process.env.FRONTEND_PORT, 57 | hmr: hmrConfig, 58 | proxy: { 59 | "^/(\\?.*)?$": proxyOptions, 60 | "^/api(/|(\\?.*)?$)": proxyOptions, 61 | }, 62 | }, 63 | }); 64 | --------------------------------------------------------------------------------