├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── config ├── __mocks__ │ ├── browser-mocks.js │ ├── file-mocks.js │ ├── gatsby.js │ └── shopify-buy.js ├── jest-preprocess.js ├── loadershim.js └── setup-test-env.js ├── docs ├── .nvmrc ├── Developer Information.md ├── gatsby-config.js ├── package.json ├── src │ ├── @horacioh │ │ └── gatsby-theme-mdx │ │ │ └── components │ │ │ └── Layout.js │ ├── assets │ │ ├── images │ │ │ ├── hipster-with-mac.svg │ │ │ ├── icon.png │ │ │ ├── only-down-example.png │ │ │ └── social-card.png │ │ └── styles │ │ │ └── highlighting.css │ ├── components │ │ ├── ControlStrip │ │ │ ├── ControlStrip.js │ │ │ └── index.js │ │ ├── ExampleWithCode.js │ │ ├── Helmet.js │ │ ├── Hero.js │ │ ├── Layout.js │ │ ├── Link.js │ │ ├── Product.js │ │ ├── examples │ │ │ ├── ExampleUseAddItemToCart.js │ │ │ ├── ExampleUseAddItemsToCart.js │ │ │ ├── ExampleUseCart.js │ │ │ ├── ExampleUseCartCount.js │ │ │ ├── ExampleUseCartItems.js │ │ │ ├── ExampleUseCheckoutUrl.js │ │ │ ├── ExampleUseRemoveItemFromCart.js │ │ │ ├── ExampleUseRemoveItemsFromCart.js │ │ │ ├── ExampleUseUpdateItemQuantity.js │ │ │ └── index.js │ │ └── index.js │ ├── content │ │ ├── Hooks.js │ │ ├── hooks │ │ │ ├── useAddItemToCart.mdx │ │ │ ├── useAddItemsToCart.mdx │ │ │ ├── useCart.mdx │ │ │ ├── useCartCount.mdx │ │ │ ├── useCartItems.mdx │ │ │ ├── useCheckoutUrl.mdx │ │ │ ├── useRemoveItemFromCart.mdx │ │ │ ├── useRemoveItemsFromCart.mdx │ │ │ └── useUpdateItemQuantity.mdx │ │ └── index.js │ ├── gatsby-plugin-theme-ui │ │ └── index.js │ ├── pages │ │ ├── 404.jsx │ │ └── index.mdx │ └── utils │ │ ├── index.js │ │ └── useProducts.js ├── static │ └── social-header.png └── yarn.lock ├── gatsby-theme-shopify-manager ├── README.md ├── defaults.js ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── index.js ├── package.json ├── src │ ├── Context.tsx │ ├── ContextProvider.tsx │ ├── __tests__ │ │ ├── .eslintrc │ │ ├── Context.test.tsx │ │ └── ContextProvider.test.tsx │ ├── hooks │ │ ├── __tests__ │ │ │ ├── .eslintrc │ │ │ ├── useAddItemToCart.test.ts │ │ │ ├── useAddItemsToCart.test.ts │ │ │ ├── useCart.test.ts │ │ │ ├── useCartCount.test.ts │ │ │ ├── useCartItems.test.ts │ │ │ ├── useCheckoutUrl.test.ts │ │ │ ├── useClientUnsafe.test.ts │ │ │ ├── useGetLineItem.test.ts │ │ │ ├── useRemoveItemFromCart.test.ts │ │ │ ├── useRemoveItemsFromCart.test.ts │ │ │ ├── useSetCartUnsafe.test.ts │ │ │ └── useUpdateItemQuantity.test.ts │ │ ├── index.ts │ │ ├── useAddItemToCart.ts │ │ ├── useAddItemsToCart.ts │ │ ├── useCart.ts │ │ ├── useCartCount.ts │ │ ├── useCartItems.ts │ │ ├── useCheckoutUrl.ts │ │ ├── useClientUnsafe.ts │ │ ├── useGetLineItem.ts │ │ ├── useRemoveItemFromCart.ts │ │ ├── useRemoveItemsFromCart.ts │ │ ├── useSetCartUnsafe.ts │ │ └── useUpdateItemQuantity.ts │ ├── index.ts │ ├── mocks │ │ ├── cart.ts │ │ ├── client.ts │ │ ├── constants.ts │ │ ├── contextWrappers │ │ │ ├── index.ts │ │ │ ├── renderHookWithContext.tsx │ │ │ ├── renderHookWithContextSynchronously.tsx │ │ │ ├── renderWithContext.tsx │ │ │ ├── types.ts │ │ │ └── wrapWithContext.tsx │ │ ├── emptyCart.ts │ │ ├── getCurrentCart.ts │ │ ├── index.ts │ │ └── purchasedCart.ts │ ├── types.ts │ └── utils │ │ ├── LocalStorage │ │ ├── LocalStorage.ts │ │ ├── __tests__ │ │ │ └── LocalStorage.test.ts │ │ ├── index.ts │ │ └── keys.ts │ │ ├── index.ts │ │ ├── types │ │ ├── __tests__ │ │ │ └── isCart.test.ts │ │ ├── index.ts │ │ └── isCart.ts │ │ └── useCoreOptions │ │ ├── __tests__ │ │ └── useCoreOptions.test.tsx │ │ ├── index.ts │ │ ├── types.ts │ │ └── useCoreOptions.ts └── tsconfig.json ├── jest.config.js ├── package.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | This came from https://www.arden.nl/setting-up-a-gatsby-js-starter-with-type-script-es-lint-prettier-and-pre-commit-hooks 3 | Some of these options might not make sense later, but for now they do. 4 | This is not meant to be considered the end-product, but the starting line. 5 | */ 6 | 7 | module.exports = { 8 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:react/recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier/@typescript-eslint', 14 | 'plugin:prettier/recommended', 15 | 'plugin:jest/recommended', 16 | 'plugin:jest/style', 17 | ], 18 | settings: { 19 | react: { 20 | version: 'detect', 21 | }, 22 | }, 23 | env: { 24 | browser: true, 25 | node: true, 26 | es6: true, 27 | 'jest/globals': true, 28 | }, 29 | plugins: ['@typescript-eslint', 'react', 'jest'], 30 | parserOptions: { 31 | ecmaFeatures: { 32 | jsx: true, 33 | }, 34 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 35 | sourceType: 'module', // Allows for the use of imports 36 | }, 37 | rules: { 38 | 'react/prop-types': 'off', // Disable prop-types as we use TypeScript for type checking 39 | '@typescript-eslint/explicit-function-return-type': 'off', 40 | }, 41 | ignorePatterns: ['node_modules/', '.cache/', 'public/'], 42 | }; 43 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: 🏗 Setup Node 12 | uses: actions/setup-node@v1.4.4 13 | with: 14 | version: 13.7.0 15 | 16 | - name: 📂 Get yarn cache directory 17 | id: yarn-cache-dir 18 | run: echo "::set-output name=dir::$(yarn cache dir)" 19 | 20 | - name: 📥 Cache node modules 21 | uses: actions/cache@v2.1.3 22 | env: 23 | cache-name: cache-node-modules 24 | with: 25 | path: ${{ steps.yarn-cache-dir.outputs.dir }} 26 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-build-${{ env.cache-name }}- 29 | ${{ runner.os }}-build- 30 | ${{ runner.os }}- 31 | 32 | - name: 📦 Install dependencies 33 | run: yarn 34 | 35 | - name: 👚 Lint 36 | run: yarn lint 37 | 38 | - name: 🏁 Type Check 39 | run: yarn type-check 40 | 41 | - name: 🔍 Run tests 42 | run: yarn test-build 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 13.7.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | public 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": false, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".git/": true, 4 | "node_modules/": false, 5 | "public/": false 6 | }, 7 | "search.exclude": { 8 | "**/.cache": true, 9 | "**/node_modules": true, 10 | "**/public": true 11 | }, 12 | "editor.rulers": [80], 13 | "editor.tabSize": 2, 14 | "editor.formatOnSave": true, 15 | "eslint.validate": [ 16 | "javascript", 17 | "javascriptreact", 18 | "typescript", 19 | "typescriptreact" 20 | ], 21 | "typescript.tsdk": "./node_modules/typescript/lib", 22 | "debug.node.autoAttach": "on", 23 | "editor.codeActionsOnSave": { 24 | "source.fixAll.eslint": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Trevor Harmon 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 | gatsby-theme-shopify-manager/README.md -------------------------------------------------------------------------------- /config/__mocks__/browser-mocks.js: -------------------------------------------------------------------------------- 1 | class LocalStorageMock { 2 | constructor() { 3 | this.store = {}; 4 | } 5 | 6 | clear() { 7 | this.store = {}; 8 | } 9 | 10 | getItem(key) { 11 | return this.store[key] || null; 12 | } 13 | 14 | setItem(key, value) { 15 | this.store[key] = value; 16 | } 17 | 18 | removeItem(key) { 19 | delete this.store[key]; 20 | } 21 | } 22 | 23 | Object.defineProperty(window, 'localStorage', { 24 | value: new LocalStorageMock(), 25 | }); 26 | -------------------------------------------------------------------------------- /config/__mocks__/file-mocks.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /config/__mocks__/gatsby.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const gatsby = jest.requireActual('gatsby'); 3 | 4 | module.exports = { 5 | ...gatsby, 6 | graphql: jest.fn(), 7 | Link: jest.fn().mockImplementation( 8 | // these props are invalid for an `a` tag 9 | ({ 10 | /* eslint-disable @typescript-eslint/no-unused-vars */ 11 | activeClassName, 12 | activeStyle, 13 | getProps, 14 | innerRef, 15 | partiallyActive, 16 | ref, 17 | replace, 18 | /* eslint-enable @typescript-eslint/no-unused-vars */ 19 | to, 20 | ...rest 21 | }) => 22 | React.createElement('a', { 23 | ...rest, 24 | href: to, 25 | }), 26 | ), 27 | StaticQuery: jest.fn(), 28 | useStaticQuery: jest.fn(), 29 | }; 30 | -------------------------------------------------------------------------------- /config/__mocks__/shopify-buy.js: -------------------------------------------------------------------------------- 1 | import {Mocks} from '../../gatsby-theme-shopify-manager/src/mocks'; 2 | const shopifyBuy = jest.requireActual('shopify-buy'); 3 | 4 | shopifyBuy.buildClient = ({storefrontAccessToken, domain}) => { 5 | if (storefrontAccessToken == null) { 6 | throw new Error('new Config() requires the option storefrontAccessToken'); 7 | } 8 | 9 | if (domain == null) { 10 | throw new Error('new Config() requires the option domain'); 11 | } 12 | 13 | return Mocks.CLIENT; 14 | }; 15 | 16 | export default shopifyBuy; 17 | -------------------------------------------------------------------------------- /config/jest-preprocess.js: -------------------------------------------------------------------------------- 1 | const babelOptions = { 2 | presets: [ 3 | 'babel-preset-gatsby', 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | }; 8 | 9 | module.exports = require('babel-jest').createTransformer(babelOptions); 10 | -------------------------------------------------------------------------------- /config/loadershim.js: -------------------------------------------------------------------------------- 1 | global.___loader = { 2 | enqueue: jest.fn(), 3 | }; 4 | -------------------------------------------------------------------------------- /config/setup-test-env.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | -------------------------------------------------------------------------------- /docs/.nvmrc: -------------------------------------------------------------------------------- 1 | 13.7.0 2 | -------------------------------------------------------------------------------- /docs/Developer Information.md: -------------------------------------------------------------------------------- 1 | # Developer Information 2 | 3 | This docs site uses [gatsby-theme-mdx](https://github.com/horacioh/gatsby-theme-mdx) to easily pull in MDX, Theme UI, and a syntax highlighting plugin. Because of that, it [shadows](https://www.gatsbyjs.org/docs/themes/shadowing/) the files of that theme. That's why the directory `@horacioh` exists–the package is a scoped package, and therefore needs the scope as the first folder name. 4 | -------------------------------------------------------------------------------- /docs/gatsby-config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: `.env`, 3 | }); 4 | 5 | module.exports = { 6 | siteMetadata: { 7 | title: 'Gatsby Theme Shopify Manager', 8 | description: `The easiest way to build a Shopify store on Gatsby.`, 9 | author: `@thetrevorharmon`, 10 | twitterHandle: `@thetrevorharmon`, 11 | siteUrl: 'https://gatsbythemeshopifymanager.com', 12 | }, 13 | plugins: [ 14 | { 15 | resolve: `gatsby-theme-shopify-manager`, 16 | options: { 17 | shopName: process.env.SHOP_NAME, 18 | accessToken: process.env.ACCESS_TOKEN, 19 | }, 20 | }, 21 | { 22 | resolve: `gatsby-source-filesystem`, 23 | options: { 24 | name: `hooks`, 25 | path: `${__dirname}/src/content/hooks/`, 26 | }, 27 | }, 28 | `gatsby-transformer-sharp`, 29 | `gatsby-plugin-sharp`, 30 | { 31 | resolve: `@horacioh/gatsby-theme-mdx`, 32 | }, 33 | { 34 | resolve: `gatsby-plugin-manifest`, 35 | options: { 36 | name: `Gatsby Theme Shopify Manager`, 37 | icon: 'src/assets/images/icon.png', 38 | }, 39 | }, 40 | `gatsby-plugin-react-helmet`, 41 | { 42 | resolve: `gatsby-plugin-google-analytics`, 43 | options: { 44 | trackingId: process.env.GOOGLE_TRACKING_ID, 45 | anonymize: true, 46 | respectDNT: true, 47 | }, 48 | }, 49 | ], 50 | }; 51 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "gatsby develop", 8 | "develop": "gatsby develop", 9 | "develop-broadcast": "gatsby develop -H 0.0.0.0", 10 | "build": "gatsby build" 11 | }, 12 | "dependencies": { 13 | "@horacioh/gatsby-theme-mdx": "^0.1.1", 14 | "dotenv": "^8.2.0", 15 | "gatsby": "^2.19.18", 16 | "gatsby-image": "^2.2.41", 17 | "gatsby-plugin-google-analytics": "^2.3.2", 18 | "gatsby-plugin-manifest": "^2.4.3", 19 | "gatsby-plugin-react-helmet": "^3.3.1", 20 | "gatsby-plugin-sharp": "^2.4.5", 21 | "gatsby-source-filesystem": "^2.2.2", 22 | "gatsby-source-shopify": "^3.2.30", 23 | "gatsby-theme-shopify-manager": "0.1.8", 24 | "gatsby-transformer-sharp": "^2.3.16", 25 | "prism-themes": "^1.3.0", 26 | "react": "^16.8.0", 27 | "react-dom": "^16.8.0", 28 | "react-helmet": "^6.0.0", 29 | "theme-ui": "^0.3.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/@horacioh/gatsby-theme-mdx/components/Layout.js: -------------------------------------------------------------------------------- 1 | import {Layout} from '../../../components'; 2 | export default Layout; 3 | -------------------------------------------------------------------------------- /docs/src/assets/images/hipster-with-mac.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetrevorharmon/gatsby-theme-shopify-manager/baefbf9c3fbaf6a1de5fb875178e5ee536cdb6d2/docs/src/assets/images/icon.png -------------------------------------------------------------------------------- /docs/src/assets/images/only-down-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetrevorharmon/gatsby-theme-shopify-manager/baefbf9c3fbaf6a1de5fb875178e5ee536cdb6d2/docs/src/assets/images/only-down-example.png -------------------------------------------------------------------------------- /docs/src/assets/images/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetrevorharmon/gatsby-theme-shopify-manager/baefbf9c3fbaf6a1de5fb875178e5ee536cdb6d2/docs/src/assets/images/social-card.png -------------------------------------------------------------------------------- /docs/src/assets/styles/highlighting.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | :root { 6 | --block-background: #ffffff; 7 | --base: #474747; 8 | --comment: #93a1a1; 9 | --punctuation: #999999; 10 | --property: #5600a6; 11 | --selector: #00a622; 12 | --operator: #a66c00; 13 | --variable: #ffcf77; 14 | --function: #ff7783; 15 | --keyword: #48b3f4; 16 | --inline: #a66c00; 17 | --inline-background: #fdfef9; 18 | } 19 | /* Generated with http://k88hudson.github.io/syntax-highlighting-theme-generator/www */ 20 | /* http://k88hudson.github.io/react-markdocs */ 21 | /** 22 | * @author k88hudson 23 | * 24 | * Based on prism.js default theme for JavaScript, CSS and HTML 25 | * Based on dabblet (http://dabblet.com) 26 | * @author Lea Verou 27 | */ 28 | /********************************************************* 29 | * General 30 | */ 31 | pre[class*='language-'], 32 | code[class*='language-'] { 33 | color: var(--base); 34 | font-size: 0.9rem; 35 | text-shadow: none; 36 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 37 | direction: ltr; 38 | text-align: left; 39 | white-space: pre; 40 | word-spacing: normal; 41 | word-break: normal; 42 | line-height: 1.5; 43 | -moz-tab-size: 4; 44 | -o-tab-size: 4; 45 | tab-size: 4; 46 | -webkit-hyphens: none; 47 | -moz-hyphens: none; 48 | -ms-hyphens: none; 49 | hyphens: none; 50 | } 51 | pre[class*='language-']::selection, 52 | code[class*='language-']::selection, 53 | pre[class*='language-']::mozselection, 54 | code[class*='language-']::mozselection { 55 | text-shadow: none; 56 | background: var(--keyword); 57 | } 58 | @media print { 59 | pre[class*='language-'], 60 | code[class*='language-'] { 61 | text-shadow: none; 62 | } 63 | } 64 | pre[class*='language-'] { 65 | padding: 1em; 66 | margin: 0.5em 0; 67 | overflow: auto; 68 | background: var(--block-background); 69 | } 70 | :not(pre) > code[class*='language-'] { 71 | padding: 0.1em 0.3em; 72 | border-radius: 0.3em; 73 | color: var(--inline); 74 | background: var(--inline-background); 75 | } 76 | /********************************************************* 77 | * Tokens 78 | */ 79 | .namespace { 80 | opacity: 0.7; 81 | } 82 | .token.comment, 83 | .token.prolog, 84 | .token.doctype, 85 | .token.cdata { 86 | color: var(--comment); 87 | } 88 | .token.punctuation { 89 | color: var(--punctuation); 90 | } 91 | .token.property, 92 | .token.tag, 93 | .token.boolean, 94 | .token.number, 95 | .token.constant, 96 | .token.symbol, 97 | .token.deleted { 98 | color: var(--property); 99 | } 100 | .token.selector, 101 | .token.attr-name, 102 | .token.string, 103 | .token.char, 104 | .token.builtin, 105 | .token.inserted { 106 | color: var(--selector); 107 | } 108 | .token.operator, 109 | .token.entity, 110 | .token.url, 111 | .language-css .token.string, 112 | .style .token.string { 113 | color: var(--operator); 114 | background: var(--block-background); 115 | } 116 | .token.atrule, 117 | .token.attr-value, 118 | .token.keyword { 119 | color: var(--keyword); 120 | } 121 | .token.function { 122 | color: var(--function); 123 | } 124 | .token.regex, 125 | .token.important, 126 | .token.variable { 127 | color: var(--variable); 128 | } 129 | .token.important, 130 | .token.bold { 131 | font-weight: bold; 132 | } 133 | .token.italic { 134 | font-style: italic; 135 | } 136 | .token.entity { 137 | cursor: help; 138 | } 139 | -------------------------------------------------------------------------------- /docs/src/components/ControlStrip/ControlStrip.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import {jsx, Box, Button} from 'theme-ui'; 3 | import { 4 | useAddItemToCart, 5 | useCartItems, 6 | useRemoveItemFromCart, 7 | useRemoveItemsFromCart, 8 | } from 'gatsby-theme-shopify-manager'; 9 | import {useProducts} from '../../utils'; 10 | 11 | export function ControlStrip({mdxType}) { 12 | return ( 13 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export function AddItemButton() { 25 | const addItemToCart = useAddItemToCart(); 26 | const products = useProducts(); 27 | 28 | async function addToCart() { 29 | const productIndex = Math.floor( 30 | Math.random() * Math.floor(products.length), 31 | ); 32 | const variantId = products[productIndex].variants[0].shopifyId; 33 | const quantity = 1; 34 | 35 | try { 36 | await addItemToCart(variantId, quantity); 37 | alert('Successfully added an item to your cart!'); 38 | } catch { 39 | alert('There was a problem adding an item to your cart.'); 40 | } 41 | } 42 | 43 | return ( 44 | 47 | ); 48 | } 49 | 50 | export function RemoveItemButton() { 51 | const removeItemFromCart = useRemoveItemFromCart(); 52 | const cartItems = useCartItems(); 53 | 54 | async function removeFromCart() { 55 | if (cartItems.length < 1) { 56 | return; 57 | } 58 | 59 | const [cartItemToRemove] = cartItems; 60 | console.log(cartItemToRemove); 61 | 62 | try { 63 | await removeItemFromCart(cartItemToRemove.variant.id); 64 | alert('Successfully removed an item from your cart!'); 65 | } catch { 66 | alert('There was a problem removing an item from your cart.'); 67 | } 68 | } 69 | 70 | return ( 71 | 74 | ); 75 | } 76 | 77 | export function EmptyCartButton() { 78 | const removeItemsFromCart = useRemoveItemsFromCart(); 79 | const cartItems = useCartItems(); 80 | 81 | async function emptyCart() { 82 | if (cartItems.length < 1) { 83 | return; 84 | } 85 | 86 | try { 87 | const variantIds = cartItems.map((cartItem) => cartItem.variant.id); 88 | await removeItemsFromCart(variantIds); 89 | alert('Successfully removed all items from your cart!'); 90 | } catch { 91 | alert('There was a problem removing an item from your cart.'); 92 | } 93 | } 94 | 95 | return ( 96 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /docs/src/components/ControlStrip/index.js: -------------------------------------------------------------------------------- 1 | export {ControlStrip} from './ControlStrip'; 2 | -------------------------------------------------------------------------------- /docs/src/components/ExampleWithCode.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import {jsx} from 'theme-ui'; 3 | import {Box} from 'theme-ui'; 4 | import {ControlStrip} from '../components'; 5 | 6 | export function ExampleWithCode({element, children}) { 7 | return ( 8 | 9 | 10 | {element} 11 | 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/components/Helmet.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetrevorharmon/gatsby-theme-shopify-manager/baefbf9c3fbaf6a1de5fb875178e5ee536cdb6d2/docs/src/components/Helmet.js -------------------------------------------------------------------------------- /docs/src/components/Hero.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import {jsx, Box, Text, Styled} from 'theme-ui'; 3 | import HipsterWithMac from '../assets/images/hipster-with-mac.svg'; 4 | import {useStaticQuery, graphql} from 'gatsby'; 5 | 6 | export function Hero() { 7 | const { 8 | site: { 9 | siteMetadata: {title, description}, 10 | }, 11 | } = useStaticQuery(graphql` 12 | query HeroQuery { 13 | site { 14 | siteMetadata { 15 | title 16 | description 17 | } 18 | } 19 | } 20 | `); 21 | 22 | const imgProps = { 23 | alt: 24 | 'A cheery-looking hipster holding a laptop wearing a yellow shirt, a black hat, and glasses', 25 | src: HipsterWithMac, 26 | }; 27 | 28 | const styles = { 29 | container: { 30 | my: 5, 31 | display: 'grid', 32 | gridTemplateColumns: ['3.5fr 1fr', '2fr 1fr'], 33 | }, 34 | header: {order: 1, my: 0, alignSelf: ['center', 'end'], maxWidth: '21rem'}, 35 | description: { 36 | color: 'heading', 37 | order: 3, 38 | mt: [1, 2], 39 | gridColumnStart: [1, 'auto'], 40 | gridColumnEnd: [3, 'auto'], 41 | }, 42 | imageContainer: { 43 | order: 2, 44 | gridColumnStart: ['auto', 2], 45 | gridRowStart: ['auto', 1], 46 | gridRowEnd: ['auto', 3], 47 | alignSelf: 'center', 48 | }, 49 | image: {maxWidth: '100%'}, 50 | }; 51 | 52 | return ( 53 | 54 | {title} 55 | {description} 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /docs/src/components/Layout.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import {Styled, jsx} from 'theme-ui'; 3 | import {useStaticQuery, graphql} from 'gatsby'; 4 | import {Link} from '../components'; 5 | import {Helmet as ReactHelmet} from 'react-helmet'; 6 | import SocialCardPath from '../assets/images/social-card.png'; 7 | 8 | const Layout = ({children}) => { 9 | const { 10 | site: { 11 | siteMetadata: {title, description, twitterHandle, siteUrl}, 12 | }, 13 | } = useStaticQuery(graphql` 14 | query SiteTitleQuery { 15 | site { 16 | siteMetadata { 17 | title 18 | description 19 | twitterHandle 20 | siteUrl 21 | } 22 | } 23 | } 24 | `); 25 | 26 | const heartEmoji = ( 27 | 28 | ❤️ 29 | 30 | ); 31 | 32 | const twitterLink = ( 33 | {twitterHandle} 34 | ); 35 | 36 | const pabloStanleyLink = ( 37 | Pablo Stanley 38 | ); 39 | 40 | const openPeepsLink = ( 41 | Open Peeps 42 | ); 43 | 44 | const meta = [ 45 | { 46 | name: 'og:title', 47 | content: title, 48 | }, 49 | { 50 | name: 'og:site_name', 51 | content: title, 52 | }, 53 | { 54 | name: 'description', 55 | content: description, 56 | }, 57 | { 58 | name: 'og:description', 59 | content: description, 60 | }, 61 | { 62 | name: 'og:url', 63 | content: siteUrl, 64 | }, 65 | { 66 | name: 'og:image', 67 | content: `${siteUrl}${SocialCardPath}`, 68 | }, 69 | { 70 | name: 'twitter:card', 71 | content: 'summary_large_image', 72 | }, 73 | { 74 | name: 'twitter:creator', 75 | content: twitterHandle, 76 | }, 77 | ]; 78 | 79 | return ( 80 | 81 | 82 |
90 |
{children}
91 | 97 |
98 |
99 | ); 100 | }; 101 | 102 | export {Layout}; 103 | -------------------------------------------------------------------------------- /docs/src/components/Link.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import {jsx, Link as ThemeUiLink} from 'theme-ui'; 3 | import {Link as GatsbyLink} from 'gatsby'; 4 | import {OutboundLink} from 'gatsby-plugin-google-analytics'; 5 | 6 | const EXTERNAL_URL_PATTERN = /^http/; 7 | 8 | export function Link({url, children, ...rest}) { 9 | const isExternalUrl = EXTERNAL_URL_PATTERN.test(url); 10 | 11 | if (isExternalUrl) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } else { 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/components/Product.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useProducts} from '../utils'; 3 | 4 | export function Product() { 5 | const [product] = useProducts(); 6 | 7 | return ( 8 |

9 | {product.title} - {product.variant.title} 10 |

11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseAddItemToCart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button} from 'theme-ui'; 3 | import {useAddItemToCart, useCartCount} from 'gatsby-theme-shopify-manager'; 4 | import {useProducts} from '../../utils'; 5 | 6 | export function ExampleUseAddItemToCart() { 7 | const cartCount = useCartCount(); 8 | const addItemToCart = useAddItemToCart(); 9 | const products = useProducts(); 10 | 11 | async function addToCart() { 12 | const variantId = products[0].variants[0].shopifyId; 13 | const quantity = 1; 14 | 15 | try { 16 | await addItemToCart(variantId, quantity); 17 | alert('Successfully added that item to your cart!'); 18 | } catch { 19 | alert('There was a problem adding that item to your cart.'); 20 | } 21 | } 22 | 23 | return ( 24 | <> 25 |

There are currently {cartCount} items in your cart.

26 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseAddItemsToCart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button} from 'theme-ui'; 3 | import {useAddItemsToCart, useCartCount} from 'gatsby-theme-shopify-manager'; 4 | import {useProducts} from '../../utils'; 5 | 6 | export function ExampleUseAddItemsToCart() { 7 | const cartCount = useCartCount(); 8 | const addItemsToCart = useAddItemsToCart(); 9 | const products = useProducts(); 10 | 11 | async function addToCart() { 12 | const items = [ 13 | { 14 | variantId: products[0].variants[0].shopifyId, 15 | quantity: 1, 16 | }, 17 | ]; 18 | 19 | try { 20 | await addItemsToCart(items); 21 | alert('Successfully added that item to your cart!'); 22 | } catch { 23 | alert('There was a problem adding that item to your cart.'); 24 | } 25 | } 26 | 27 | return ( 28 | <> 29 |

There are currently {cartCount} items in your cart.

30 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseCart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useCart} from 'gatsby-theme-shopify-manager'; 3 | 4 | export function ExampleUseCart() { 5 | const cart = useCart(); 6 | 7 | if (cart == null) { 8 | return

The cart object is currently null.

; 9 | } 10 | 11 | const cartDate = new Date(cart.createdAt).toLocaleDateString(); 12 | 13 | return ( 14 |

15 | Your cart was created on {cartDate}. 16 |
17 | You have ${cart.totalPrice} worth of products in your cart. 18 |

19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseCartCount.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useCartCount} from 'gatsby-theme-shopify-manager'; 3 | 4 | export function ExampleUseCartCount() { 5 | const cartCount = useCartCount(); 6 | 7 | return

Your cart has {cartCount} items.

; 8 | } 9 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseCartItems.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useCartItems} from 'gatsby-theme-shopify-manager'; 3 | 4 | export function ExampleUseCartItems() { 5 | const keyModifier = `ExampleUseCartItems`; 6 | const cartItems = useCartItems(); 7 | 8 | if (cartItems.length < 1) { 9 | return

Your cart is empty.

; 10 | } 11 | 12 | return ( 13 | <> 14 |

Your cart has the following items:

15 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseCheckoutUrl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useCheckoutUrl} from 'gatsby-theme-shopify-manager'; 3 | import {Link} from '../../components'; 4 | 5 | export function ExampleUseCheckoutUrl() { 6 | const checkoutUrl = useCheckoutUrl(); 7 | 8 | return checkoutUrl == null ? ( 9 |

There is no active checkout.

10 | ) : ( 11 |

12 | 13 | Complete Your Order → 14 | 15 |

16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseRemoveItemFromCart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button} from 'theme-ui'; 3 | import { 4 | useRemoveItemFromCart, 5 | useCartItems, 6 | } from 'gatsby-theme-shopify-manager'; 7 | 8 | export function ExampleUseRemoveItemFromCart() { 9 | const keyModifier = 'ExampleUseRemoveItemFromCart'; 10 | const cartItems = useCartItems(); 11 | const removeItemFromCart = useRemoveItemFromCart(); 12 | 13 | async function removeFromCart() { 14 | if (cartItems.length < 1) { 15 | return; 16 | } 17 | const variantId = cartItems[0].variant.id; 18 | 19 | try { 20 | await removeItemFromCart(variantId); 21 | alert('Successfully removed an item from your cart!'); 22 | } catch { 23 | alert('There was a problem removing that item from your cart.'); 24 | } 25 | } 26 | 27 | const cartMarkup = 28 | cartItems.length > 0 ? ( 29 | <> 30 |

Your cart has the following items:

31 | 38 | 39 | ) : ( 40 |

Your cart is empty.

41 | ); 42 | 43 | return ( 44 | <> 45 | {cartMarkup} 46 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseRemoveItemsFromCart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button} from 'theme-ui'; 3 | import { 4 | useRemoveItemsFromCart, 5 | useCartItems, 6 | } from 'gatsby-theme-shopify-manager'; 7 | 8 | export function ExampleUseRemoveItemsFromCart() { 9 | const keyModifier = 'ExampleUseRemoveItemsFromCart'; 10 | const cartItems = useCartItems(); 11 | const removeItemsFromCart = useRemoveItemsFromCart(); 12 | 13 | async function removeFromCart() { 14 | if (cartItems.length < 1) { 15 | return; 16 | } 17 | const variantId = cartItems[0].variant.id; 18 | 19 | try { 20 | await removeItemsFromCart([variantId]); 21 | alert('Successfully removed an item from your cart!'); 22 | } catch { 23 | alert('There was a problem removing that item from your cart.'); 24 | } 25 | } 26 | 27 | const cartMarkup = 28 | cartItems.length > 0 ? ( 29 | <> 30 |

Your cart has the following items:

31 | 38 | 39 | ) : ( 40 |

Your cart is empty.

41 | ); 42 | 43 | return ( 44 | <> 45 | {cartMarkup} 46 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /docs/src/components/examples/ExampleUseUpdateItemQuantity.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Flex, Button, Input} from 'theme-ui'; 3 | import { 4 | useUpdateItemQuantity, 5 | useCartItems, 6 | } from 'gatsby-theme-shopify-manager'; 7 | 8 | export function ExampleUseUpdateItemQuantity() { 9 | const [quantity, setQuantity] = useState(1); 10 | const [item] = useCartItems(); 11 | const updateItemQuantity = useUpdateItemQuantity(); 12 | 13 | async function updateQuantity() { 14 | if (item == null) { 15 | return; 16 | } 17 | 18 | const variantId = item.variant.id; 19 | 20 | try { 21 | await updateItemQuantity(variantId, quantity); 22 | alert('Successfully updated the item quantity!'); 23 | } catch { 24 | alert("There was a problem updating that item's quantity."); 25 | } 26 | } 27 | 28 | function submitForm(event) { 29 | event.preventDefault(); 30 | event.stopPropagation(); 31 | updateQuantity(); 32 | } 33 | 34 | const itemMarkup = 35 | item == null ? ( 36 |

Your cart is empty.

37 | ) : ( 38 |

39 | {item.title} - {item.variant.title} ({item.quantity}) 40 |

41 | ); 42 | 43 | const formMarkup = ( 44 | 45 | setQuantity(Number(event.target.value))} 50 | /> 51 | 52 | 53 | ); 54 | 55 | return ( 56 | <> 57 | {itemMarkup} 58 | {formMarkup} 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /docs/src/components/examples/index.js: -------------------------------------------------------------------------------- 1 | export {ExampleUseCart} from './ExampleUseCart'; 2 | export {ExampleUseCartItems} from './ExampleUseCartItems'; 3 | export {ExampleUseCartCount} from './ExampleUseCartCount'; 4 | export {ExampleUseCheckoutUrl} from './ExampleUseCheckoutUrl'; 5 | export {ExampleUseAddItemsToCart} from './ExampleUseAddItemsToCart'; 6 | export {ExampleUseAddItemToCart} from './ExampleUseAddItemToCart'; 7 | export {ExampleUseRemoveItemsFromCart} from './ExampleUseRemoveItemsFromCart'; 8 | export {ExampleUseRemoveItemFromCart} from './ExampleUseRemoveItemFromCart'; 9 | export {ExampleUseUpdateItemQuantity} from './ExampleUseUpdateItemQuantity'; 10 | -------------------------------------------------------------------------------- /docs/src/components/index.js: -------------------------------------------------------------------------------- 1 | export {Product} from './Product'; 2 | export {Layout} from './Layout'; 3 | export {ExampleWithCode} from './ExampleWithCode'; 4 | export {ControlStrip} from './ControlStrip'; 5 | export {Link} from './Link'; 6 | export {Hero} from './Hero'; 7 | export * from './examples'; 8 | -------------------------------------------------------------------------------- /docs/src/content/Hooks.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useStaticQuery, graphql} from 'gatsby'; 3 | import {MDXRenderer} from 'gatsby-plugin-mdx'; 4 | import {Link} from '../components'; 5 | 6 | export function Hooks() { 7 | const { 8 | allFile: {edges}, 9 | } = useStaticQuery( 10 | graphql` 11 | query HooksDocumentationFiles { 12 | allFile(filter: {sourceInstanceName: {eq: "hooks"}}) { 13 | edges { 14 | node { 15 | name 16 | childMdx { 17 | frontmatter { 18 | order 19 | } 20 | body 21 | tableOfContents 22 | } 23 | } 24 | } 25 | } 26 | } 27 | `, 28 | ); 29 | 30 | const hooksDocumentationFiles = edges 31 | .map((edge) => edge.node) 32 | .sort((firstNode, secondNode) => { 33 | const firstNodeFrontmatter = firstNode.childMdx.frontmatter; 34 | const secondNodeFrontmatter = secondNode.childMdx.frontmatter; 35 | 36 | if (firstNodeFrontmatter.order > secondNodeFrontmatter.order) { 37 | return 1; 38 | } 39 | 40 | if (firstNodeFrontmatter.order < secondNodeFrontmatter.order) { 41 | return -1; 42 | } 43 | 44 | if (firstNodeFrontmatter.title > secondNodeFrontmatter.title) { 45 | return 1; 46 | } 47 | 48 | return -1; 49 | }); 50 | 51 | return ( 52 | <> 53 | 64 | {hooksDocumentationFiles.map((hook) => { 65 | return {hook.childMdx.body}; 66 | })} 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useAddItemToCart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 6 3 | --- 4 | 5 | import {ExampleUseAddItemToCart, ExampleWithCode} from '../../components'; 6 | 7 | ### useAddItemToCart() 8 | 9 | The `useAddItemToCart` is similar to the `useAddItemsToCart`, but is only for a single item at a time. The hook returns a function that accepts three arguments: `variantId`, `quantity`, and (optionally) an array of `customAttributes`. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {Button} from 'theme-ui'; 16 | import {useAddItemToCart, useCartCount} from 'gatsby-theme-shopify-manager'; 17 | 18 | export function ExampleUseAddItemToCart() { 19 | const cartCount = useCartCount(); 20 | const addItemToCart = useAddItemToCart(); 21 | 22 | async function addToCart() { 23 | const variantId = 'some_variant_id'; 24 | const quantity = 1; 25 | 26 | try { 27 | await addItemToCart(variantId, quantity); 28 | alert('Successfully added that item to your cart!'); 29 | } catch { 30 | alert('There was a problem adding that item to your cart.'); 31 | } 32 | } 33 | 34 | return ( 35 | <> 36 |

There are currently {cartCount} items in your cart.

37 | 40 | 41 | ); 42 | } 43 | ``` 44 |
45 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useAddItemsToCart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 5 3 | --- 4 | 5 | import {ExampleUseAddItemsToCart, ExampleWithCode} from '../../components'; 6 | 7 | ### useAddItemsToCart() 8 | 9 | The `useAddItemsToCart` hook allows you to add multiple items to the cart at a single time. The hook returns a function that accepts an array of objects with keys `variantId` and `quantity`. It returns a `void` promise that will throw if it encounters an error. You can optionally include an array of `customAttributes` with each item. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {Button} from 'theme-ui'; 16 | import {useAddItemsToCart, useCartCount} from 'gatsby-theme-shopify-manager'; 17 | 18 | export function ExampleUseAddItemsToCart() { 19 | const cartCount = useCartCount(); 20 | const addItemsToCart = useAddItemsToCart(); 21 | 22 | async function addToCart() { 23 | const items = [ 24 | { 25 | variantId: 'some_variant_id', 26 | quantity: 1, 27 | }, 28 | ]; 29 | 30 | try { 31 | await addItemsToCart(items); 32 | alert('Successfully added that item to your cart!'); 33 | } catch { 34 | alert('There was a problem adding that item to your cart.'); 35 | } 36 | } 37 | 38 | return ( 39 | <> 40 |

There are currently {cartCount} items in your cart.

41 | 44 | 45 | ); 46 | } 47 | 48 | ``` 49 |
50 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useCart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | --- 4 | 5 | import {ExampleUseCart, ExampleWithCode} from '../../components'; 6 | 7 | ### useCart() 8 | 9 | The most basic hook is the `useCart()` hook. This hook gives you access to the current cart state (or null, if there is no cart state). From this object you can get access to the line items, the total amounts, and additional checkout-related information. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {useCart} from 'gatsby-theme-shopify-manager'; 16 | 17 | export function ExampleUseCart() { 18 | const cart = useCart(); 19 | 20 | if (cart == null) { 21 | return

The cart object is currently null.

; 22 | } 23 | 24 | const cartDate = new Date(cart.createdAt).toLocaleDateString(); 25 | 26 | return ( 27 |

28 | Your cart was created on {cartDate}. 29 |
30 | You have ${cart.totalPrice} worth of products in your cart. 31 |

32 | ); 33 | } 34 | ``` 35 |
36 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useCartCount.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 3 3 | --- 4 | 5 | import {ExampleUseCartCount, ExampleWithCode} from '../../components'; 6 | 7 | ### useCartCount() 8 | 9 | The `useCartCount()` hook provides the number of items currently in the cart. This hook returns 0 if the cart is null (and will always return a number). This method does not return just `cartItems.length`, but sums the quantity of each variant in the cart. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {useCartCount} from 'gatsby-theme-shopify-manager'; 16 | 17 | export function ExampleUseCartCount() { 18 | const cartCount = useCartCount(); 19 | 20 | return

Your cart has {cartCount} items.

; 21 | } 22 | ``` 23 |
24 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useCartItems.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | --- 4 | 5 | import {ExampleUseCartItems, ExampleWithCode} from '../../components'; 6 | 7 | ### useCartItems() 8 | 9 | The `useCartItems()` hook provides access to the items currently in the cart. This hook always returns an array. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {useCartItems} from 'gatsby-theme-shopify-manager'; 16 | 17 | export function ExampleUseCartItems() { 18 | const cartItems = useCartItems(); 19 | 20 | if (cartItems.length < 1) { 21 | return

Your cart is empty.

; 22 | } 23 | 24 | return ( 25 | <> 26 |

Your cart has the following items:

27 | 34 | 35 | ); 36 | } 37 | ``` 38 |
39 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useCheckoutUrl.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 4 3 | --- 4 | 5 | import {ExampleUseCheckoutUrl, ExampleWithCode} from '../../components'; 6 | 7 | ### useCheckoutUrl() 8 | 9 | The `useCheckoutUrl()` hook provides the checkout url that is associated with the current cart. It returns `null` when the cart is `null`, and otherwise returns a `string`. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {useCheckoutUrl} from 'gatsby-theme-shopify-manager'; 16 | 17 | export function ExampleUseCheckoutUrl() { 18 | const checkoutUrl = useCheckoutUrl(); 19 | 20 | return checkoutUrl == null ? ( 21 |

There is no active checkout.

22 | ) : ( 23 |

24 | 25 | Complete Your Order → 26 | 27 |

28 | ); 29 | } 30 | 31 | ``` 32 |
33 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useRemoveItemFromCart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 8 3 | --- 4 | 5 | import {ExampleUseRemoveItemFromCart, ExampleWithCode} from '../../components'; 6 | 7 | ### useRemoveItemFromCart() 8 | 9 | The `useRemoveItemFromCart` is similar to the `useRemoveItemsFromCart` hook, but is only for a single item at a time. The hook returns a function that accepts a single argument: `variantId`. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {Button} from 'theme-ui'; 16 | import { 17 | useRemoveItemFromCart, 18 | useCartItems, 19 | } from 'gatsby-theme-shopify-manager'; 20 | 21 | export function ExampleUseRemoveItemFromCart() { 22 | const cartItems = useCartItems(); 23 | const removeItemFromCart = useRemoveItemFromCart(); 24 | 25 | async function removeFromCart() { 26 | if (cartItems.length < 1) { 27 | return; 28 | } 29 | const variantId = cartItems[0].variant.id; 30 | 31 | try { 32 | await removeItemFromCart(variantId); 33 | alert('Successfully removed an item from your cart!'); 34 | } catch { 35 | alert('There was a problem removing that item from your cart.'); 36 | } 37 | } 38 | 39 | const cartMarkup = 40 | cartItems.length > 0 ? ( 41 | <> 42 |

Your cart has the following items:

43 | 50 | 51 | ) : ( 52 |

Your cart is empty.

53 | ); 54 | 55 | return ( 56 | <> 57 | {cartMarkup} 58 | 61 | 62 | ); 63 | } 64 | ``` 65 |
66 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useRemoveItemsFromCart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 7 3 | --- 4 | 5 | import {ExampleUseRemoveItemsFromCart, ExampleWithCode} from '../../components'; 6 | 7 | ### useRemoveItemsFromCart() 8 | 9 | The `useRemoveItemFromCart` hook allows you to remove multiple items from the cart at a single time. The hook returns a function that accepts an array of `variantId` strings. It returns a `void` promise that will throw if it encounters an error. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React from 'react'; 15 | import {Button} from 'theme-ui'; 16 | import { 17 | useRemoveItemsFromCart, 18 | useCartItems, 19 | } from 'gatsby-theme-shopify-manager'; 20 | 21 | export function ExampleUseRemoveItemsFromCart() { 22 | const cartItems = useCartItems(); 23 | const removeItemsFromCart = useRemoveItemsFromCart(); 24 | 25 | async function removeFromCart() { 26 | if (cartItems.length < 1) { 27 | return; 28 | } 29 | const variantId = cartItems[0].variant.id; 30 | 31 | try { 32 | await removeItemsFromCart([variantId]); 33 | alert('Successfully removed an item from your cart!'); 34 | } catch { 35 | alert('There was a problem removing that item from your cart.'); 36 | } 37 | } 38 | 39 | const cartMarkup = 40 | cartItems.length > 0 ? ( 41 | <> 42 |

Your cart has the following items:

43 | 50 | 51 | ) : ( 52 |

Your cart is empty.

53 | ); 54 | 55 | return ( 56 | <> 57 | {cartMarkup} 58 | 61 | 62 | ); 63 | } 64 | ``` 65 |
66 | -------------------------------------------------------------------------------- /docs/src/content/hooks/useUpdateItemQuantity.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | order: 9 3 | --- 4 | 5 | import {ExampleUseUpdateItemQuantity, ExampleWithCode} from '../../components'; 6 | 7 | ### useUpdateItemQuantity() 8 | 9 | The `useUpdateItemQuantity()` hook returns a function that updates the quantity of a lineitem currently in the cart. The returned function accepts two arguments: `variantId` and `quantity`. It returns a `void` Promise that throws if it encounters an error. If `0` is passed in as the quantity, it removes the item from the cart. 10 | 11 | }> 12 | 13 | ```javascript 14 | import React, {useState} from 'react'; 15 | import {Flex, Button, Input} from 'theme-ui'; 16 | import { 17 | useUpdateItemQuantity, 18 | useCartItems, 19 | } from 'gatsby-theme-shopify-manager'; 20 | 21 | export function ExampleUseUpdateItemQuantity() { 22 | const [quantity, setQuantity] = useState(1); 23 | const [item] = useCartItems(); 24 | const updateItemQuantity = useUpdateItemQuantity(); 25 | 26 | async function updateQuantity() { 27 | if (item == null) { 28 | return; 29 | } 30 | 31 | const variantId = item.variant.id; 32 | 33 | try { 34 | await updateItemQuantity(variantId, quantity); 35 | alert('Successfully updated the item quantity!'); 36 | } catch { 37 | alert("There was a problem updating that item's quantity."); 38 | } 39 | } 40 | 41 | const itemMarkup = 42 | item == null ? ( 43 |

Your cart is empty.

44 | ) : ( 45 |

46 | {item.title} - {item.variant.title} ({item.quantity}) 47 |

48 | ); 49 | 50 | const formMarkup = ( 51 | 52 | setQuantity(Number(event.target.value))} 57 | /> 58 | 59 | 60 | ); 61 | 62 | return ( 63 | <> 64 | {itemMarkup} 65 | {formMarkup} 66 | 67 | ); 68 | } 69 | ``` 70 |
71 | -------------------------------------------------------------------------------- /docs/src/content/index.js: -------------------------------------------------------------------------------- 1 | export {Hooks} from './Hooks'; 2 | -------------------------------------------------------------------------------- /docs/src/gatsby-plugin-theme-ui/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | breakpoints: ['28em', '52em', '64em'], 3 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512], 4 | fonts: { 5 | body: 6 | 'system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif', 7 | heading: 'inherit', 8 | monospace: 'Menlo, monospace', 9 | }, 10 | fontSizes: [12, 14, 20, 26, 32, 40], 11 | fontWeights: { 12 | body: 400, 13 | heading: 800, 14 | bold: 700, 15 | }, 16 | lineHeights: { 17 | body: 1.5, 18 | heading: 1.125, 19 | }, 20 | colors: { 21 | background: '#F3F0EB', 22 | backgroundDarker: '#d8cfaf', 23 | heading: '#A66C00', 24 | text: '#353535', 25 | primary: '#FFCF77', 26 | secondary: '#48B3F4', 27 | muted: '#f6f6f6', 28 | }, 29 | text: { 30 | heading: { 31 | fontFamily: 'heading', 32 | lineHeight: 'heading', 33 | fontWeight: 'heading', 34 | color: 'heading', 35 | }, 36 | }, 37 | styles: { 38 | root: { 39 | fontSize: '18px', 40 | color: 'text', 41 | fontFamily: 'body', 42 | lineHeight: 'body', 43 | fontWeight: 'body', 44 | overflow: 'hidden', 45 | backgroundColor: 'background', 46 | }, 47 | h1: { 48 | variant: 'text.heading', 49 | fontSize: [4, 5], 50 | color: 'black', 51 | }, 52 | h2: { 53 | variant: 'text.heading', 54 | fontSize: [3, 4], 55 | mt: 6, 56 | mb: 3, 57 | }, 58 | h3: { 59 | variant: 'text.heading', 60 | fontSize: [2, 3], 61 | mt: 5, 62 | mb: 3, 63 | }, 64 | h4: { 65 | variant: 'text.heading', 66 | mt: 4, 67 | mb: 2, 68 | fontSize: 2, 69 | }, 70 | h5: { 71 | variant: 'text.heading', 72 | fontSize: 1, 73 | }, 74 | h6: { 75 | variant: 'text.heading', 76 | fontSize: 0, 77 | }, 78 | pre: { 79 | fontFamily: 'monospace', 80 | overflowX: 'auto', 81 | code: { 82 | color: 'inherit', 83 | }, 84 | }, 85 | code: { 86 | fontFamily: 'monospace', 87 | fontSize: 'inherit', 88 | }, 89 | a: { 90 | color: 'heading', 91 | }, 92 | table: { 93 | mt: 4, 94 | width: '100%', 95 | borderCollapse: 'separate', 96 | borderSpacing: 0, 97 | }, 98 | th: { 99 | textAlign: 'left', 100 | borderBottomStyle: 'solid', 101 | pr: 3, 102 | }, 103 | td: { 104 | textAlign: 'left', 105 | borderBottomStyle: 'solid', 106 | borderBottom: '1px solid #aaa', 107 | p: 2, 108 | pl: 0, 109 | pr: 3, 110 | }, 111 | blockquote: { 112 | backgroundColor: 'primary', 113 | position: 'relative', 114 | margin: 0, 115 | padding: 3, 116 | p: { 117 | margin: 0, 118 | padding: 0, 119 | }, 120 | '::before': { 121 | content: '""', 122 | display: 'block', 123 | borderLeft: '1rem solid transparent', 124 | borderRight: '1rem solid', 125 | borderRightColor: 'background', 126 | borderTop: '1rem solid transparent', 127 | height: 0, 128 | width: 0, 129 | position: 'absolute', 130 | right: 0, 131 | bottom: 0, 132 | zIndex: 2, 133 | }, 134 | }, 135 | }, 136 | alerts: { 137 | callout: { 138 | color: '#555', 139 | bg: 'muted', 140 | border: '1px solid #aaa', 141 | }, 142 | }, 143 | buttons: { 144 | primary: { 145 | color: 'text', 146 | '&:hover': { 147 | cursor: 'pointer', 148 | }, 149 | }, 150 | controlStrip: { 151 | fontSize: 1, 152 | px: 2, 153 | py: 1, 154 | fontWeight: 600, 155 | backgroundColor: 'secondary', 156 | '&:hover': { 157 | cursor: 'pointer', 158 | }, 159 | }, 160 | }, 161 | cards: { 162 | primary: { 163 | overflow: 'hidden', 164 | background: 'white', 165 | padding: 0, 166 | borderRadius: 4, 167 | boxShadow: '0 0 8px rgba(0, 0, 0, 0.125)', 168 | }, 169 | }, 170 | forms: { 171 | input: { 172 | border: 'none', 173 | backgroundColor: 'background', 174 | }, 175 | }, 176 | }; 177 | -------------------------------------------------------------------------------- /docs/src/pages/404.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import {Styled, jsx, Box} from 'theme-ui'; 3 | import {Layout, Link} from '../components'; 4 | 5 | function ErrorPage() { 6 | return ( 7 | 8 | 9 | 10 | Darn! 11 | Looks like you found a bad link. 12 | Go Home 13 | 14 | 15 | 16 | ); 17 | } 18 | 19 | export default ErrorPage; 20 | -------------------------------------------------------------------------------- /docs/src/pages/index.mdx: -------------------------------------------------------------------------------- 1 | import '../assets/styles/highlighting.css'; 2 | 3 | import {Alert, Text, Box, Card, Image, Styled} from 'theme-ui'; 4 | import {Hooks} from '../content'; 5 | import {Hero, Link} from '../components'; 6 | import OnlyDownExample from '../assets/images/only-down-example.png'; 7 | 8 | 9 | 10 | 11 | 12 | Gatsby Theme Shopify Manager is a Gatsby theme that manages the data connections between Shopify and your Gatsby storefront. All you have to do is: 13 | 14 | 1. Install the theme 15 | 2. Provide API credentials 16 | 3. Import hooks 17 | 18 | And then start coding. 🚀 19 | 20 | This is a data theme, not a UI theme. It makes setting up a Shopify cart and buyer flow simple (including managing state), so you can focus on making it look great. 21 | 22 | > Want to see this in action? _This page_ is a working example, as well as documentation. Read on to see it! 23 | 24 | 25 | 26 | ## Getting Started 27 | 28 | To start using the theme, install it with your package manager of choice: 29 | 30 | ```bash 31 | yarn add gatsby-theme-shopify-manager gatsby-source-shopify 32 | ``` 33 | 34 | To start using it, open your `gatsby-config` file and include your Shop name and access token from the Storefront API. 35 | 36 | ```javascript 37 | { 38 | resolve: `gatsby-theme-shopify-manager`, 39 | options: { 40 | shopName: 'your shop name', 41 | accessToken: 'your storefront API access token', 42 | }, 43 | }, 44 | ``` 45 | 46 | The options you pass to this theme are used to configure both `gatsby-source-shopify` and the [shopify-buy](https://shopify.github.io/js-buy-sdk/) client. 47 | 48 | ### Configuration options 49 | 50 | There are four options to configure this theme, with only the first two being required. 51 | 52 | 1. [shopName](/#shopname) 53 | 1. [accessToken](/#accesstoken) 54 | 1. [shouldConfigureSourcePlugin](/#shouldConfigureSourcePlugin) 55 | 1. [shouldWrapRootElementWithProvider](/#shouldwraprootelementwithprovider) 56 | 57 | In case you're looking for a quick copy and paste 👇: 58 | 59 | ```javascript 60 | { 61 | resolve: `gatsby-theme-shopify-manager`, 62 | options: { 63 | shopName: 'your-shop-name', // or custom domain 64 | accessToken: 'your-api-access-token', 65 | shouldConfigureSourcePlugin: true, // default 66 | shouldWrapRootElementWithProvider: true, // default 67 | }, 68 | }, 69 | ``` 70 | 71 | #### shopName 72 | 73 | This is the first part of the default Shopify domain. If your domain is `my-store.myshopify.com`, the shopName would be `my-store`. This value is required unless you pass `false` to both `shouldConfigureSourcePlugin` and `shouldWrapRootElementWithProvider`. 74 | 75 | If you're using a custom domain with Shopify, you should enter your custom domain instead (e.g. `mystore.com`). Make sure to only include the name and domain, and omit the protocol (`http`) and any trailing slashes. 76 | ```javascript 77 | { 78 | resolve: `gatsby-theme-shopify-manager`, 79 | options: { 80 | shopName: 'my-store', // or mystore.com 81 | }, 82 | }, 83 | ``` 84 | 85 | #### accessToken 86 | 87 | This is the Storefront API token that you get when you make a new Shopify app. This value is required unless you pass `false` to both `shouldConfigureSourcePlugin` and `shouldWrapRootElementWithProvider`. 88 | ```javascript 89 | { 90 | resolve: `gatsby-theme-shopify-manager`, 91 | options: { 92 | accessToken: '12lg(@!l129gj12p[' 93 | }, 94 | }, 95 | ``` 96 | 97 | #### shouldConfigureSourcePlugin 98 | 99 | By default, `gatsby-theme-shopify-manager` passes a configuration object to the `gatsby-source-shopify` plugin in `gatsby-config`. If you need to do advanced configuration of that plugin, pass `false` to this option. From there, you can set up and configure your source plugin as you please. 100 | ```javascript 101 | { 102 | resolve: `gatsby-theme-shopify-manager`, 103 | options: { 104 | // default value is true 105 | shouldConfigureSourcePlugin: false 106 | }, 107 | }, 108 | ``` 109 | 110 | #### shouldWrapRootElementWithProvider 111 | 112 | By default, `gatsby-theme-shopify-manager` wraps the application in a ``, and passes the `shopName` and `accessToken` provided to the theme options through to the provider. Pass `false` to this option to prevent this behavior. 113 | ```javascript 114 | { 115 | resolve: `gatsby-theme-shopify-manager`, 116 | options: { 117 | // default value is true 118 | shouldWrapRootElementWithProvider: false 119 | }, 120 | }, 121 | ``` 122 | 123 | ## Context Provider 124 | 125 | The Shopify buy client and current cart state are managed using React context. By default, the application is wrapped by the Provider and the `shopName` and `accessToken` are pulled from the config options and passed to it. However, in some cases, it might be preferable to manage the provider. 126 | 127 | > By default, `gatsby-theme-shopify-manager` wraps the application in a provider. If you want to manage this yourself, pass `shouldWrapRootElementWithProvider: false` to the theme options. If you don't, you'll have multiple providers that may result in unintended side-effects. 128 | 129 | To use the provider, import it and pass `shopName` and `accessToken` to it as props. 130 | 131 | ```javascript 132 | import React from 'react'; 133 | import {ContextProvider} from 'gatsby-theme-shopify-manager'; 134 | export const App = ({children}) => { 135 | const shopName = 'some-shop-name'; 136 | const accessToken = 'some-access-token'; 137 | 138 | return ( 139 | 140 | {children} 141 | 142 | ); 143 | }; 144 | ``` 145 | 146 | ## Hooks 147 | 148 | The main export of this package are the hooks that you can use. Here are the hooks you can use: 149 | 150 | 151 | 152 | ## Escape Hooks 153 | 154 | In addition to the normal hooks, there are two 'escape' hooks. These hooks allow access to setting the cart state and the client object that is used to interact with Shopify. It's important to note that these are considered experiemental–**using these hooks may result in unintended side-effects.** 155 | 156 | ### useClientUnsafe 157 | 158 | The `useClientUnsafe` hook returns the client object currently held in the context. From there you can call methods on it to enable more functionality. Shopify has all [the documentation](https://shopify.github.io/js-buy-sdk/) for what you can do with the client object. Example usage: 159 | 160 | ```javascript 161 | import React from 'react'; 162 | import {useClientUnsafe} from 'gatsby-theme-shopify-manager'; 163 | 164 | export function ExampleUseClientUnsafe() { 165 | const client = useClientUnsafe(); 166 | // do work with the client here 167 | } 168 | ``` 169 | 170 | ### useSetCartUnsafe 171 | 172 | The `useSetCartUnsafe` returns a function that allows the user to set the current cart state. You can use it similar to the function returned from a `useState` destructure. This is useful for interactions with the `client` object that return an updated cart object. Example usage: 173 | 174 | ```javascript 175 | import React from 'react'; 176 | import {useClientUnsafe, useSetCartUnsafe} from 'gatsby-theme-shopify-manager'; 177 | 178 | export function ExampleUseSetCartUnsafe() { 179 | const client = useClientUnsafe(); 180 | const setCart = useSetCartUnsafe(); 181 | 182 | async function changeCart() { 183 | const newCart = await client.doSomeMethodThatReturnsACartObject(); 184 | setCart(newCart); 185 | } 186 | 187 | changeCart(); 188 | } 189 | ``` 190 | 191 | ## Examples 192 | 193 | 198 | 199 | 200 | 204 | Only Down 205 | 206 |

Only Down is an example site that shows how to use the gatsby-theme-shopify-manager plugin.

207 |
208 |
209 | 210 | ## Contributing & Issues 211 | 212 | Want to add a feature, or report a bug? Head over to the [GitHub repo](https://github.com/thetrevorharmon/gatsby-theme-shopify-manager) to jump in! 213 | -------------------------------------------------------------------------------- /docs/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export {useProducts} from './useProducts'; 2 | -------------------------------------------------------------------------------- /docs/src/utils/useProducts.js: -------------------------------------------------------------------------------- 1 | import {useStaticQuery, graphql} from 'gatsby'; 2 | 3 | export function useProducts() { 4 | const { 5 | allShopifyProduct: {nodes: rawProducts}, 6 | } = useStaticQuery( 7 | graphql` 8 | query ProductQuery { 9 | allShopifyProduct(filter: {}, limit: 3) { 10 | nodes { 11 | shopifyId 12 | description 13 | descriptionHtml 14 | title 15 | variants { 16 | shopifyId 17 | image { 18 | id 19 | } 20 | selectedOptions { 21 | name 22 | value 23 | } 24 | title 25 | price 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | ); 32 | 33 | const products = rawProducts.map((product) => { 34 | const [variant] = product.variants; 35 | return { 36 | ...product, 37 | variant, 38 | }; 39 | }); 40 | 41 | return products; 42 | } 43 | -------------------------------------------------------------------------------- /docs/static/social-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetrevorharmon/gatsby-theme-shopify-manager/baefbf9c3fbaf6a1de5fb875178e5ee536cdb6d2/docs/static/social-header.png -------------------------------------------------------------------------------- /docs/yarn.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetrevorharmon/gatsby-theme-shopify-manager/baefbf9c3fbaf6a1de5fb875178e5ee536cdb6d2/docs/yarn.lock -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/README.md: -------------------------------------------------------------------------------- 1 | ![Gatsby Theme Shopify_Manager](https://gatsbythemeshopifymanager.com/social-header.png) 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/a69b3855-3f3c-437f-bd8e-90567d58106a/deploy-status)](https://app.netlify.com/sites/gatsby-theme-shopify-manager/deploys) 4 | 5 | # Looking for maintainers 6 | 7 | This project is currently not maintained. If you actively use this plugin, please consider becoming a maintainer. 8 | 9 | ## Quickstart guide 10 | 11 | Install this with npm: 12 | 13 | ```bash 14 | npm install gatsby-theme-shopify-manager 15 | ``` 16 | 17 | Or with yarn: 18 | 19 | ```bash 20 | yarn add gatsby-theme-shopify-manager 21 | ``` 22 | 23 | Set up your `gatsby-config.js`: 24 | 25 | ```javascript 26 | { 27 | resolve: `gatsby-theme-shopify-manager`, 28 | options: { 29 | shopName: `your-shop-name`, 30 | accessToken: `your-storefront-api-access-token`, 31 | }, 32 | }, 33 | ``` 34 | 35 | Import a hook: 36 | 37 | ```javascript 38 | import {useCart} from 'gatsby-theme-shopify-manager'; 39 | ``` 40 | 41 | Start coding. 🚀 42 | 43 | ## Full documentation 44 | 45 | The full docs are found at [https://gatsbythemeshopifymanager.com/](https://gatsbythemeshopifymanager.com/). 46 | 47 | ## Contributing 48 | 49 | To contribute to this repo, pull the repo and ask for the appropriate `.env` values for the `/docs` site. Then to start the project, simply run `yarn start` at the project root. 50 | 51 | To add a new version, take the following steps: 52 | 53 | 1. Increment the `/docs` version of `gatsby-theme-shopify-manager` to whatever it will be. 54 | 2. Stage any changes you want to be part of the commit. 55 | 3. Run `yarn version` within the `gatsby-theme-shopify-manager` directory. 56 | 4. Change the version number to the appropriate release number (major, minor, patch). 57 | 5. Run `git push --tags` and `git push`. 58 | 6. Run `npm publish`. 59 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/defaults.js: -------------------------------------------------------------------------------- 1 | // got this pattern/idea from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-theme-blog-core/gatsby-config.js 2 | module.exports = (themeOptions) => { 3 | const shouldConfigureSourcePlugin = 4 | themeOptions.shouldConfigureSourcePlugin != null 5 | ? themeOptions.shouldConfigureSourcePlugin 6 | : true; 7 | 8 | const shouldWrapRootElementWithProvider = 9 | themeOptions.shouldWrapRootElementWithProvider != null 10 | ? themeOptions.shouldWrapRootElementWithProvider 11 | : true; 12 | 13 | const shopName = themeOptions.shopName || null; 14 | const accessToken = themeOptions.accessToken || null; 15 | 16 | return { 17 | shouldConfigureSourcePlugin, 18 | shouldWrapRootElementWithProvider, 19 | shopName, 20 | accessToken, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ContextProvider} from './src'; 3 | import withDefaults from './defaults'; 4 | 5 | export const wrapRootElement = ({element}, themeOptions) => { 6 | const { 7 | shouldWrapRootElementWithProvider, 8 | shopName, 9 | accessToken, 10 | } = withDefaults(themeOptions); 11 | 12 | if (shouldWrapRootElementWithProvider === false) { 13 | return element; 14 | } 15 | 16 | const missingApiInformation = shopName == null || accessToken == null; 17 | if (missingApiInformation) { 18 | throw new Error( 19 | 'gatsby-theme-shopify-manager: You forgot to pass in a shopName or accessToken to the theme options', 20 | ); 21 | } 22 | 23 | return ( 24 | 25 | {element} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/gatsby-config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const withDefaults = require(`./defaults`); 3 | 4 | module.exports = (themeOptions) => { 5 | const options = withDefaults(themeOptions); 6 | const { 7 | shouldConfigureSourcePlugin, 8 | shouldWrapRootElementWithProvider, 9 | shopName, 10 | accessToken, 11 | } = options; 12 | 13 | const needsApiInformation = 14 | shouldConfigureSourcePlugin === true || 15 | shouldWrapRootElementWithProvider === true; 16 | const missingApiInformation = shopName == null || accessToken == null; 17 | 18 | if (needsApiInformation && missingApiInformation) { 19 | throw new Error( 20 | 'gatsby-theme-shopify-manager: You forgot to pass in a shopName or accessToken to the theme options', 21 | ); 22 | } 23 | 24 | const shopifySourcePlugin = shouldConfigureSourcePlugin 25 | ? { 26 | resolve: `gatsby-source-shopify`, 27 | options: { 28 | shopName, 29 | accessToken, 30 | }, 31 | } 32 | : null; 33 | 34 | const plugins = ['gatsby-plugin-typescript', shopifySourcePlugin].filter( 35 | Boolean, 36 | ); 37 | 38 | return { 39 | plugins, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/gatsby-node.js: -------------------------------------------------------------------------------- 1 | /* 2 | In order to access user options throughout the app, we have to 3 | add them as a node within the Graphql. 4 | 5 | This creates a type called "CoreOptions" on the GraphQL schema. 6 | */ 7 | exports.createSchemaCustomization = ({actions}) => { 8 | const {createTypes} = actions; 9 | createTypes(`type 10 | CoreOptions implements Node { 11 | shopName: String 12 | accessToken: String 13 | }`); 14 | }; 15 | 16 | /* 17 | In order to access user options throughout the app, we have to 18 | add them as a node within the Graphql. 19 | 20 | This takes options passed in to a child's gatsby-config and creates 21 | a node for them. 22 | 23 | Further reading: 24 | • https://www.gatsbyjs.org/docs/node-apis/#sourceNodes 25 | • https://www.christopherbiscardi.com/post/applying-theme-options-using-custom-configuration-nodes/ 26 | • https://www.erichowey.dev/writing/examples-of-using-options-in-gatsby-themes/ 27 | */ 28 | exports.sourceNodes = ( 29 | {actions: {createNode}, createContentDigest}, 30 | {shopName = ``, accessToken = ``}, 31 | ) => { 32 | const coreOptions = { 33 | shopName, 34 | accessToken, 35 | }; 36 | 37 | createNode({ 38 | ...coreOptions, 39 | id: `gatsby-theme-shopify-manager`, 40 | parent: null, 41 | children: [], 42 | internal: { 43 | description: `Core Options`, 44 | type: `CoreOptions`, 45 | content: JSON.stringify(coreOptions), 46 | contentDigest: createContentDigest(JSON.stringify(coreOptions)), 47 | }, 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/index.js: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-theme-shopify-manager", 3 | "description": "The easiest way to build a Shopify shop on Gatsby.", 4 | "version": "0.1.8", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "type-check": "tsc --noEmit" 9 | }, 10 | "peerDependencies": { 11 | "gatsby": "^2.19.18", 12 | "react": "^16.8.0", 13 | "react-dom": "^16.8.0" 14 | }, 15 | "dependencies": { 16 | "@types/react": "^16.9.20", 17 | "@types/react-dom": "^16.9.5", 18 | "@types/shopify-buy": "^1.4.3", 19 | "gatsby-plugin-typescript": "^2.1.27", 20 | "shopify-buy": "^2.9.0", 21 | "typescript": "^3.7.5" 22 | }, 23 | "devDependencies": { 24 | "gatsby": "^2.19.18", 25 | "react": "^16.8.0", 26 | "react-dom": "^16.8.0" 27 | }, 28 | "keywords": [ 29 | "gatsby", 30 | "gatsby-plugin", 31 | "gatsby-theme", 32 | "shopify" 33 | ], 34 | "author": "Trevor Harmon (http://thetrevorhamon.com)", 35 | "bugs": { 36 | "url": "https://github.com/thetrevorharmon/gatsby-theme-shopify-manager/issues" 37 | }, 38 | "homepage": "https://gatsbythemeshopifymanager.com/", 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/thetrevorharmon/gatsby-theme-shopify-manager.git", 42 | "directory": "gatsby-theme-shopify-manager" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/Context.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ShopifyBuy from 'shopify-buy'; 3 | 4 | interface ContextShape { 5 | client: ShopifyBuy.Client | null; 6 | cart: ShopifyBuy.Cart | null; 7 | setCart: React.Dispatch>; 8 | } 9 | 10 | export const Context = React.createContext({ 11 | client: null, 12 | cart: null, 13 | setCart: () => { 14 | throw Error('You forgot to wrap this in a Provider object'); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/ContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import ShopifyBuy from 'shopify-buy'; 3 | import {Context} from './Context'; 4 | import {LocalStorage, LocalStorageKeys} from './utils'; 5 | 6 | interface Props { 7 | shopName: string; 8 | accessToken: string; 9 | children: React.ReactNode; 10 | } 11 | 12 | export function ContextProvider({shopName, accessToken, children}: Props) { 13 | if (shopName == null || accessToken == null) { 14 | throw new Error( 15 | 'Unable to build shopify-buy client object. Please make sure that your access token and domain are correct.', 16 | ); 17 | } 18 | 19 | const initialCart = LocalStorage.getInitialCart(); 20 | const [cart, setCart] = useState(initialCart); 21 | 22 | const isCustomDomain = shopName.includes('.'); 23 | 24 | const client = ShopifyBuy.buildClient({ 25 | storefrontAccessToken: accessToken, 26 | domain: isCustomDomain ? shopName : `${shopName}.myshopify.com`, 27 | }); 28 | 29 | useEffect(() => { 30 | async function getNewCart() { 31 | const newCart = await client.checkout.create(); 32 | setCart(newCart); 33 | } 34 | 35 | async function refreshExistingCart(cartId: string) { 36 | try { 37 | const refreshedCart = await client.checkout.fetch(cartId); 38 | 39 | if (refreshedCart == null) { 40 | return getNewCart(); 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 44 | // @ts-ignore 45 | const cartHasBeenPurchased = refreshedCart.completedAt != null; 46 | 47 | if (cartHasBeenPurchased) { 48 | getNewCart(); 49 | } else { 50 | setCart(refreshedCart); 51 | } 52 | } catch (error) { 53 | console.error(error); 54 | } 55 | } 56 | 57 | if (cart == null) { 58 | getNewCart(); 59 | } else { 60 | refreshExistingCart(String(cart.id)); 61 | } 62 | }, []); 63 | 64 | useEffect(() => { 65 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(cart)); 66 | }, [cart]); 67 | 68 | return ( 69 | 76 | {children} 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "@typescript-eslint/ban-ts-ignore": "off", 4 | "@typescript-eslint/no-non-null-assertion": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/__tests__/Context.test.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import {render} from '@testing-library/react'; 3 | import {Context} from '../Context'; 4 | 5 | describe('Context', () => { 6 | it('returns null as the default value for the client', () => { 7 | function MockComponent() { 8 | const {client} = useContext(Context); 9 | const content = client === null ? 'pass' : 'fail'; 10 | 11 | return

{content}

; 12 | } 13 | 14 | const {getAllByText} = render(); 15 | expect(getAllByText('pass')).toBeTruthy(); 16 | }); 17 | 18 | it('returns null as the default value for the cart', () => { 19 | function MockComponent() { 20 | const {cart} = useContext(Context); 21 | const content = cart === null ? 'pass' : 'fail'; 22 | 23 | return

{content}

; 24 | } 25 | 26 | const {getAllByText} = render(); 27 | expect(getAllByText('pass')).toBeTruthy(); 28 | }); 29 | 30 | it('throws an error when calling the initial value for setCart', () => { 31 | function MockComponent() { 32 | const {setCart} = useContext(Context); 33 | 34 | try { 35 | setCart(null); 36 | } catch (error) { 37 | return

{error.message}

; 38 | } 39 | 40 | return

fail

; 41 | } 42 | 43 | const {getAllByText} = render(); 44 | expect( 45 | getAllByText('You forgot to wrap this in a Provider object'), 46 | ).toBeTruthy(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/__tests__/ContextProvider.test.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect} from 'react'; 2 | import {render, waitFor} from '@testing-library/react'; 3 | import {Context} from '../Context'; 4 | import {ContextProvider} from '../ContextProvider'; 5 | import {Mocks} from '../mocks'; 6 | import {LocalStorage, LocalStorageKeys, isCart} from '../utils'; 7 | import ShopifyBuy from 'shopify-buy'; 8 | 9 | function MockComponent() { 10 | const {cart} = useContext(Context); 11 | return

{cart?.id}

; 12 | } 13 | 14 | afterEach(() => { 15 | LocalStorage.set(LocalStorageKeys.CART, ''); 16 | jest.clearAllMocks(); 17 | }); 18 | 19 | describe('ContextProvider', () => { 20 | it('throws an error if the accessToken is missing', () => { 21 | function MockComponent() { 22 | const {client} = useContext(Context); 23 | return

{typeof client}

; 24 | } 25 | 26 | const originalError = console.error; 27 | console.error = jest.fn(); 28 | 29 | expect(() => 30 | render( 31 | 36 | 37 | , 38 | ), 39 | ).toThrow(); 40 | 41 | console.error = originalError; 42 | }); 43 | 44 | it('throws an error if the shopName is missing', () => { 45 | const originalError = console.error; 46 | console.error = jest.fn(); 47 | 48 | expect(() => 49 | render( 50 | 55 | 56 | , 57 | ), 58 | ).toThrow(); 59 | 60 | console.error = originalError; 61 | }); 62 | 63 | it('passes the shopName and accessToken to the shopify-buy client', async () => { 64 | const shopifyBuySpy = jest.spyOn(ShopifyBuy, 'buildClient'); 65 | 66 | render( 67 | 71 | 72 | , 73 | ); 74 | 75 | await waitFor(() => 76 | expect(shopifyBuySpy).toHaveBeenCalledWith({ 77 | storefrontAccessToken: Mocks.ACCESS_TOKEN, 78 | domain: Mocks.MYSHOPIFY_DOMAIN, 79 | }), 80 | ); 81 | }); 82 | 83 | it('appends ".myshopify.com" to shopName when shopName is not a custom domain', async () => { 84 | const shopifyBuySpy = jest.spyOn(ShopifyBuy, 'buildClient'); 85 | 86 | render( 87 | 91 | 92 | , 93 | ); 94 | 95 | await waitFor(() => 96 | expect(shopifyBuySpy).toHaveBeenCalledWith({ 97 | storefrontAccessToken: Mocks.ACCESS_TOKEN, 98 | domain: Mocks.MYSHOPIFY_DOMAIN, 99 | }), 100 | ); 101 | }); 102 | 103 | it('does not append ".myshopify.com" to shopName when shopName is a custom domain', async () => { 104 | const shopifyBuySpy = jest.spyOn(ShopifyBuy, 'buildClient'); 105 | 106 | render( 107 | 111 | 112 | , 113 | ); 114 | 115 | await waitFor(() => 116 | expect(shopifyBuySpy).toHaveBeenCalledWith({ 117 | storefrontAccessToken: Mocks.ACCESS_TOKEN, 118 | domain: Mocks.SHOP_NAME_WITH_CUSTOM_DOMAIN, 119 | }), 120 | ); 121 | }); 122 | 123 | it('builds a shopify-buy client', async () => { 124 | const shopifyBuySpy = jest.spyOn(ShopifyBuy, 'buildClient'); 125 | 126 | render( 127 | 131 | 132 | , 133 | ); 134 | 135 | await waitFor(() => expect(shopifyBuySpy).toHaveBeenCalled()); 136 | }); 137 | 138 | it('provides a client object to the consumer', () => { 139 | function MockComponent() { 140 | const {client} = useContext(Context); 141 | 142 | if (client == null) { 143 | throw new Error('Client is undefined'); 144 | } 145 | 146 | return

pass

; 147 | } 148 | 149 | const wrapper = render( 150 | 154 | 155 | , 156 | ); 157 | 158 | expect(wrapper.findByText('pass')).toBeTruthy(); 159 | }); 160 | 161 | it('provides a cart object and setCart function to the consumer', () => { 162 | function MockComponent() { 163 | const {cart, setCart} = useContext(Context); 164 | 165 | try { 166 | setCart(cart); 167 | } catch { 168 | throw new Error('setCart is using default value'); 169 | } 170 | 171 | return

pass

; 172 | } 173 | 174 | const wrapper = render( 175 | 179 | 180 | , 181 | ); 182 | 183 | expect(wrapper.findByText('pass')).toBeTruthy(); 184 | }); 185 | 186 | it('checks local storage to see if a cart object exists', async () => { 187 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 188 | 189 | const localStorageSpy = jest.spyOn(LocalStorage, 'getInitialCart'); 190 | 191 | function MockComponent() { 192 | const {cart} = useContext(Context); 193 | const content = cart != null ? cart.id : 'fail'; 194 | 195 | return

{content}

; 196 | } 197 | 198 | const {getByText} = render( 199 | 203 | 204 | , 205 | ); 206 | 207 | await waitFor(() => { 208 | expect(localStorageSpy).toHaveBeenCalled(); 209 | expect(getByText(Mocks.CART.id)).toBeTruthy(); 210 | }); 211 | }); 212 | 213 | it('uses the cart in local storage as the initial value if it exists', async () => { 214 | const initialCart = {...Mocks.CART, id: 'testInitialCart'}; 215 | const localStorageSpy = jest.spyOn(LocalStorage, 'getInitialCart'); 216 | const createCartSpy = jest.spyOn(Mocks.CLIENT.checkout, 'create'); 217 | 218 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(initialCart)); 219 | 220 | function MockComponent() { 221 | const {cart} = useContext(Context); 222 | const content = cart != null ? cart.id : 'fail'; 223 | 224 | return

{content}

; 225 | } 226 | 227 | const {asFragment} = render( 228 | 232 | 233 | , 234 | ); 235 | 236 | const firstRender = asFragment(); 237 | 238 | await waitFor(() => { 239 | expect(localStorageSpy).toHaveBeenCalled(); 240 | expect(createCartSpy).not.toHaveBeenCalled(); 241 | expect(firstRender.textContent).toBe(initialCart.id); 242 | }); 243 | }); 244 | 245 | it('creates a new cart object if there is no initial cart object', async () => { 246 | const createCartSpy = jest.spyOn(Mocks.CLIENT.checkout, 'create'); 247 | 248 | function MockComponent() { 249 | const {cart} = useContext(Context); 250 | const content = cart != null ? cart.id : 'fail'; 251 | 252 | return

{content}

; 253 | } 254 | 255 | const {asFragment} = render( 256 | 260 | 261 | , 262 | ); 263 | 264 | await waitFor(() => { 265 | expect(createCartSpy).toHaveBeenCalled(); 266 | expect(asFragment().textContent).toBe(Mocks.CART.id); 267 | }); 268 | }); 269 | 270 | it('refreshes the cart object if there is an initial cart object', async () => { 271 | const refreshedCart = {...Mocks.CART, id: 'refreshedCartId'}; 272 | (Mocks.CLIENT.checkout.fetch as jest.Mock).mockImplementationOnce( 273 | () => refreshedCart, 274 | ); 275 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 276 | const fetchCartSpy = jest.spyOn(Mocks.CLIENT.checkout, 'fetch'); 277 | 278 | function MockComponent() { 279 | const {cart} = useContext(Context); 280 | const content = cart != null ? cart.id : 'fail'; 281 | 282 | return

{content}

; 283 | } 284 | 285 | const {asFragment} = render( 286 | 290 | 291 | , 292 | ); 293 | 294 | await waitFor(() => { 295 | expect(fetchCartSpy).toHaveBeenCalled(); 296 | expect(asFragment().textContent).toBe(refreshedCart.id); 297 | }); 298 | }); 299 | 300 | it('drops the cart object and creates a new one if the refreshed cart shows that it has been purchased', async () => { 301 | (Mocks.CLIENT.checkout.fetch as jest.Mock).mockImplementationOnce( 302 | () => Mocks.PURCHASED_CART, 303 | ); 304 | (Mocks.CLIENT.checkout.create as jest.Mock).mockImplementationOnce( 305 | () => Mocks.EMPTY_CART, 306 | ); 307 | 308 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 309 | const fetchCartSpy = jest.spyOn(Mocks.CLIENT.checkout, 'fetch'); 310 | const createCartSpy = jest.spyOn(Mocks.CLIENT.checkout, 'create'); 311 | 312 | function MockComponent() { 313 | const {cart} = useContext(Context); 314 | const content = cart != null ? cart.id : 'fail'; 315 | 316 | return

{content}

; 317 | } 318 | 319 | const {asFragment} = render( 320 | 324 | 325 | , 326 | ); 327 | 328 | await waitFor(() => { 329 | expect(fetchCartSpy).toHaveBeenCalled(); 330 | expect(createCartSpy).toHaveBeenCalled(); 331 | expect(asFragment().textContent).toBe(Mocks.EMPTY_CART.id); 332 | }); 333 | }); 334 | 335 | it('creates a new cart if the refreshed cart returns null', async () => { 336 | (Mocks.CLIENT.checkout.fetch as jest.Mock).mockImplementationOnce( 337 | () => null, 338 | ); 339 | (Mocks.CLIENT.checkout.create as jest.Mock).mockImplementationOnce( 340 | () => Mocks.EMPTY_CART, 341 | ); 342 | 343 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 344 | const fetchCartSpy = jest.spyOn(Mocks.CLIENT.checkout, 'fetch'); 345 | const createCartSpy = jest.spyOn(Mocks.CLIENT.checkout, 'create'); 346 | 347 | function MockComponent() { 348 | const {cart} = useContext(Context); 349 | const content = cart != null ? cart.id : 'fail'; 350 | 351 | return

{content}

; 352 | } 353 | 354 | const {asFragment} = render( 355 | 359 | 360 | , 361 | ); 362 | 363 | await waitFor(() => { 364 | expect(fetchCartSpy).toHaveBeenCalled(); 365 | expect(createCartSpy).toHaveBeenCalled(); 366 | expect(asFragment().textContent).toBe(Mocks.EMPTY_CART.id); 367 | }); 368 | }); 369 | 370 | it('saves the cart object in local storage every time it changes', async () => { 371 | const localStorageSpy = jest.spyOn(LocalStorage, 'set'); 372 | const newCart = { 373 | ...Mocks.CART, 374 | id: 'newCart', 375 | }; 376 | 377 | function MockComponent() { 378 | const {setCart} = useContext(Context); 379 | 380 | useEffect(() => { 381 | if (isCart(newCart)) { 382 | setCart(newCart); 383 | } 384 | }, []); 385 | 386 | return

Content

; 387 | } 388 | 389 | render( 390 | 394 | 395 | , 396 | ); 397 | 398 | await waitFor(() => { 399 | expect(localStorageSpy).toHaveBeenCalledTimes(3); 400 | }); 401 | }); 402 | }); 403 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "@typescript-eslint/ban-ts-ignore": "off", 4 | "@typescript-eslint/no-non-null-assertion": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useAddItemToCart.test.ts: -------------------------------------------------------------------------------- 1 | import {act} from '@testing-library/react-hooks'; 2 | import {Mocks, getCurrentCart, renderHookWithContext} from '../../mocks'; 3 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 4 | import {useAddItemToCart} from '../useAddItemToCart'; 5 | 6 | afterEach(() => { 7 | LocalStorage.set(LocalStorageKeys.CART, ''); 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | describe('useAddItemToCart()', () => { 12 | it('adds the item to the cart', async () => { 13 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 14 | const localStorageSpy = jest.spyOn(LocalStorage, 'set'); 15 | 16 | const {result, waitForNextUpdate} = await renderHookWithContext(() => 17 | useAddItemToCart(), 18 | ); 19 | 20 | act(() => { 21 | result.current('newVariantId', 1); 22 | }); 23 | await waitForNextUpdate(); 24 | 25 | const cart = getCurrentCart(); 26 | 27 | // @ts-ignore 28 | expect(cart.lineItems.slice(-1)[0].variantId).toBe('newVariantId'); 29 | expect(localStorageSpy).toHaveBeenCalledTimes(4); 30 | }); 31 | 32 | it('throws an error if the given line item has no variant id', async () => { 33 | const {result} = await renderHookWithContext(() => useAddItemToCart()); 34 | 35 | // @ts-ignore 36 | await expect(result.current()).rejects.toThrow('Missing variantId in item'); 37 | }); 38 | 39 | it('throws an error if the given line item has no quantity', async () => { 40 | const {result} = await renderHookWithContext(() => useAddItemToCart()); 41 | 42 | // @ts-ignore 43 | await expect(result.current('some_id')).rejects.toThrow( 44 | 'Missing quantity in item with variant id: some_id', 45 | ); 46 | }); 47 | 48 | it('throws an error if the given line item has a quantity that is not numeric', async () => { 49 | const {result} = await renderHookWithContext(() => useAddItemToCart()); 50 | 51 | await expect( 52 | // @ts-ignore 53 | result.current('some_id', 'one'), 54 | ).rejects.toThrow( 55 | 'Quantity is not a number in item with variant id: some_id', 56 | ); 57 | }); 58 | 59 | it('throws an error if the given line item has a quantity that is less than one', async () => { 60 | const {result} = await renderHookWithContext(() => useAddItemToCart()); 61 | 62 | await expect( 63 | // @ts-ignore 64 | result.current('some_id', 0), 65 | ).rejects.toThrow( 66 | 'Quantity must not be less than one in item with variant id: some_id', 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useAddItemsToCart.test.ts: -------------------------------------------------------------------------------- 1 | import {act} from '@testing-library/react-hooks'; 2 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 3 | import {Mocks, getCurrentCart, renderHookWithContext} from '../../mocks'; 4 | import {useAddItemsToCart} from '../useAddItemsToCart'; 5 | 6 | afterEach(() => { 7 | LocalStorage.set(LocalStorageKeys.CART, ''); 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | describe('useAddItemsToCart()', () => { 12 | it('returns true if the items are added to the cart', async () => { 13 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 14 | const localStorageSpy = jest.spyOn(LocalStorage, 'set'); 15 | 16 | const {result, waitForNextUpdate} = await renderHookWithContext(() => 17 | useAddItemsToCart(), 18 | ); 19 | 20 | act(() => { 21 | result.current([ 22 | { 23 | variantId: 'newVariantId', 24 | quantity: 1, 25 | }, 26 | ]); 27 | }); 28 | await waitForNextUpdate(); 29 | 30 | const cart = getCurrentCart(); 31 | 32 | // @ts-ignore 33 | expect(cart.lineItems.slice(-1)[0].variantId).toBe('newVariantId'); 34 | expect(localStorageSpy).toHaveBeenCalledTimes(4); 35 | }); 36 | 37 | it('throws an error if there are no line items', async () => { 38 | const {result} = await renderHookWithContext(() => useAddItemsToCart()); 39 | 40 | await expect(result.current([])).rejects.toThrow( 41 | 'Must include at least one line item, empty line items found', 42 | ); 43 | }); 44 | 45 | it('throws an error if the given line item has no variant id', async () => { 46 | const {result} = await renderHookWithContext(() => useAddItemsToCart()); 47 | 48 | // @ts-ignore 49 | await expect(result.current([{quantity: 1}])).rejects.toThrow( 50 | 'Missing variantId in item', 51 | ); 52 | }); 53 | 54 | it('throws an error if the given line item has no quantity', async () => { 55 | const {result} = await renderHookWithContext(() => useAddItemsToCart()); 56 | 57 | // @ts-ignore 58 | await expect(result.current([{variantId: 'some_id'}])).rejects.toThrow( 59 | 'Missing quantity in item with variant id: some_id', 60 | ); 61 | }); 62 | 63 | it('throws an error if the given line item has a quantity that is not numeric', async () => { 64 | const {result} = await renderHookWithContext(() => useAddItemsToCart()); 65 | 66 | await expect( 67 | // @ts-ignore 68 | result.current([{variantId: 'some_id', quantity: 'one'}]), 69 | ).rejects.toThrow( 70 | 'Quantity is not a number in item with variant id: some_id', 71 | ); 72 | }); 73 | 74 | it('throws an error if the given line item has a quantity that is less than one', async () => { 75 | const {result} = await renderHookWithContext(() => useAddItemsToCart()); 76 | 77 | await expect( 78 | // @ts-ignore 79 | result.current([{variantId: 'some_id', quantity: 0}]), 80 | ).rejects.toThrow( 81 | 'Quantity must not be less than one in item with variant id: some_id', 82 | ); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useCart.test.ts: -------------------------------------------------------------------------------- 1 | import {renderHook} from '@testing-library/react-hooks'; 2 | import {Mocks, renderHookWithContext} from '../../mocks'; 3 | import {useCart} from '../useCart'; 4 | 5 | describe('useCart()', () => { 6 | it('returns the cart object', async () => { 7 | const {result} = await renderHookWithContext(() => useCart()); 8 | expect(result.current).toBe(Mocks.CART); 9 | }); 10 | 11 | it('returns null if it is not wrapped in a provider', async () => { 12 | const {result} = renderHook(() => useCart()); 13 | expect(result.current).toBeNull(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useCartCount.test.ts: -------------------------------------------------------------------------------- 1 | import {Mocks, renderHookWithContext} from '../../mocks'; 2 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 3 | import {useCartCount} from '../useCartCount'; 4 | 5 | afterEach(() => { 6 | LocalStorage.set(LocalStorageKeys.CART, ''); 7 | jest.clearAllMocks(); 8 | }); 9 | 10 | describe('useCartCount()', () => { 11 | it('returns the total number of items in the cart, factoring in quantity per variant', async () => { 12 | const {result} = await renderHookWithContext(() => useCartCount()); 13 | 14 | const lineItemVariantQuantity = Mocks.CART.lineItems.reduce( 15 | (quantity, lineItem) => { 16 | return lineItem.quantity + quantity; 17 | }, 18 | 0, 19 | ); 20 | 21 | expect(result.current).toBe(lineItemVariantQuantity); 22 | }); 23 | 24 | it('returns 0 if the cart is null or empty', async () => { 25 | (Mocks.CLIENT.checkout.fetch as jest.Mock).mockImplementationOnce( 26 | () => Mocks.EMPTY_CART, 27 | ); 28 | 29 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.EMPTY_CART)); 30 | const {result} = await renderHookWithContext(() => useCartCount(), { 31 | shouldSetInitialCart: false, 32 | }); 33 | 34 | expect(result.current).toBe(0); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useCartItems.test.ts: -------------------------------------------------------------------------------- 1 | import {Mocks, renderHookWithContext} from '../../mocks'; 2 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 3 | import {useCartItems} from '../useCartItems'; 4 | 5 | afterEach(() => { 6 | LocalStorage.set(LocalStorageKeys.CART, ''); 7 | jest.clearAllMocks(); 8 | }); 9 | 10 | describe('useCartItems()', () => { 11 | it('returns the items in the cart', async () => { 12 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 13 | const {result} = await renderHookWithContext(() => useCartItems()); 14 | 15 | expect(result.current).toHaveLength(Mocks.CART.lineItems.length); 16 | }); 17 | 18 | it('returns an empty array if the cart is null or empty', async () => { 19 | (Mocks.CLIENT.checkout.fetch as jest.Mock).mockImplementationOnce( 20 | () => Mocks.EMPTY_CART, 21 | ); 22 | 23 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.EMPTY_CART)); 24 | const {result} = await renderHookWithContext(() => useCartItems(), { 25 | shouldSetInitialCart: false, 26 | }); 27 | 28 | expect(result.current).toHaveLength(0); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useCheckoutUrl.test.ts: -------------------------------------------------------------------------------- 1 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 2 | import {Mocks, renderHookWithContext} from '../../mocks'; 3 | import {useCheckoutUrl} from '../useCheckoutUrl'; 4 | 5 | afterEach(() => { 6 | LocalStorage.set(LocalStorageKeys.CART, ''); 7 | jest.clearAllMocks(); 8 | }); 9 | 10 | describe('useCheckoutUrl()', () => { 11 | it('returns the checkout URL from the cart', async () => { 12 | const {result} = await renderHookWithContext(() => useCheckoutUrl()); 13 | 14 | expect(result.current).toBe(Mocks.CHECKOUT_URL); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useClientUnsafe.test.ts: -------------------------------------------------------------------------------- 1 | import {renderHook} from '@testing-library/react-hooks'; 2 | import {Mocks, renderHookWithContext} from '../../mocks'; 3 | import {useClientUnsafe} from '../useClientUnsafe'; 4 | 5 | describe('useClientUnsafe()', () => { 6 | it('returns the client object', async () => { 7 | const {result} = await renderHookWithContext(() => useClientUnsafe()); 8 | expect(result.current).toBe(Mocks.CLIENT); 9 | }); 10 | 11 | it('returns null if it is not wrapped in a provider', async () => { 12 | const {result} = renderHook(() => useClientUnsafe()); 13 | expect(result.current).toBeNull(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useGetLineItem.test.ts: -------------------------------------------------------------------------------- 1 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 2 | import {Mocks, renderHookWithContext} from '../../mocks'; 3 | import {useGetLineItem} from '../useGetLineItem'; 4 | 5 | afterEach(() => { 6 | LocalStorage.set(LocalStorageKeys.CART, ''); 7 | jest.clearAllMocks(); 8 | }); 9 | 10 | describe('useGetLineItem()', () => { 11 | it('returns the item from the cart', async () => { 12 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 13 | const {result} = await renderHookWithContext(() => useGetLineItem()); 14 | 15 | expect(result.current(Mocks.VARIANT_ID_IN_CART)).toMatchObject({ 16 | title: "Men's Down Jacket", 17 | }); 18 | }); 19 | 20 | it('returns null if the cart is empty', async () => { 21 | (Mocks.CLIENT.checkout.fetch as jest.Mock).mockImplementationOnce( 22 | () => Mocks.EMPTY_CART, 23 | ); 24 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.EMPTY_CART)); 25 | 26 | const {result} = await renderHookWithContext(() => useGetLineItem(), { 27 | shouldSetInitialCart: false, 28 | }); 29 | 30 | expect(result.current(Mocks.VARIANT_ID_IN_CART)).toBeNull(); 31 | }); 32 | 33 | it('returns null if it cannot find the item', async () => { 34 | (Mocks.CLIENT.checkout.fetch as jest.Mock).mockImplementationOnce( 35 | () => Mocks.EMPTY_CART, 36 | ); 37 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.EMPTY_CART)); 38 | 39 | const {result} = await renderHookWithContext(() => useGetLineItem(), { 40 | shouldSetInitialCart: false, 41 | }); 42 | 43 | expect(result.current('some_wrong_id')).toBeNull(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useRemoveItemFromCart.test.ts: -------------------------------------------------------------------------------- 1 | import {act} from '@testing-library/react-hooks'; 2 | import {Mocks, getCurrentCart, renderHookWithContext} from '../../mocks'; 3 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 4 | import {useRemoveItemFromCart} from '../useRemoveItemFromCart'; 5 | 6 | afterEach(() => { 7 | LocalStorage.set(LocalStorageKeys.CART, ''); 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | describe('useRemoveItemFromCart()', () => { 12 | it('removes the item from the cart', async () => { 13 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 14 | const localStorageSpy = jest.spyOn(LocalStorage, 'set'); 15 | 16 | const {result, waitForNextUpdate} = await renderHookWithContext(() => 17 | useRemoveItemFromCart(), 18 | ); 19 | 20 | act(() => { 21 | result.current(Mocks.VARIANT_ID_IN_CART); 22 | }); 23 | await waitForNextUpdate(); 24 | 25 | const cart = getCurrentCart(); 26 | 27 | function findInLineItems(variantId: string) { 28 | const result = cart.lineItems 29 | .map((lineItem) => { 30 | // @ts-ignore 31 | if (lineItem.variant.id === variantId) { 32 | return lineItem.id; 33 | } 34 | return null; 35 | }) 36 | .filter(Boolean); 37 | return result; 38 | } 39 | 40 | expect(findInLineItems(Mocks.VARIANT_ID_IN_CART)).toHaveLength(0); 41 | expect(cart.lineItems).toHaveLength(Mocks.CART.lineItems.length - 1); 42 | expect(localStorageSpy).toHaveBeenCalledTimes(4); 43 | }); 44 | 45 | it('throws an error if there is no variantId passed to the function', async () => { 46 | const {result} = await renderHookWithContext(() => useRemoveItemFromCart()); 47 | 48 | // @ts-ignore 49 | await expect(result.current()).rejects.toThrow( 50 | 'VariantId must not be blank or null', 51 | ); 52 | }); 53 | 54 | it('throws an error if a given variant Id is not found in the cart', async () => { 55 | const {result} = await renderHookWithContext(() => useRemoveItemFromCart()); 56 | 57 | // @ts-ignore 58 | await expect(result.current('bogus_id')).rejects.toThrow( 59 | 'Could not find line item in cart with variant id: bogus_id', 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useRemoveItemsFromCart.test.ts: -------------------------------------------------------------------------------- 1 | import {act} from '@testing-library/react-hooks'; 2 | import {Mocks, getCurrentCart, renderHookWithContext} from '../../mocks'; 3 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 4 | import {useRemoveItemsFromCart} from '../useRemoveItemsFromCart'; 5 | 6 | afterEach(() => { 7 | LocalStorage.set(LocalStorageKeys.CART, ''); 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | describe('useRemoveItemsFromCart()', () => { 12 | it('removes the items from the cart', async () => { 13 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 14 | const localStorageSpy = jest.spyOn(LocalStorage, 'set'); 15 | 16 | const {result, waitForNextUpdate} = await renderHookWithContext(() => 17 | useRemoveItemsFromCart(), 18 | ); 19 | 20 | act(() => { 21 | result.current([Mocks.VARIANT_ID_IN_CART]); 22 | }); 23 | await waitForNextUpdate(); 24 | 25 | const cart = getCurrentCart(); 26 | 27 | function findInLineItems(variantId: string) { 28 | const result = cart.lineItems 29 | .map((lineItem) => { 30 | // @ts-ignore 31 | if (lineItem.variant.id === variantId) { 32 | return lineItem.id; 33 | } 34 | return null; 35 | }) 36 | .filter(Boolean); 37 | return result; 38 | } 39 | 40 | expect(findInLineItems(Mocks.VARIANT_ID_IN_CART)).toHaveLength(0); 41 | expect(cart.lineItems).toHaveLength(Mocks.CART.lineItems.length - 1); 42 | expect(localStorageSpy).toHaveBeenCalledTimes(4); 43 | }); 44 | 45 | it('throws an error if there are no variant Ids passed to the function', async () => { 46 | const {result} = await renderHookWithContext(() => 47 | useRemoveItemsFromCart(), 48 | ); 49 | 50 | // @ts-ignore 51 | await expect(result.current([])).rejects.toThrow( 52 | 'Must include at least one item to remove', 53 | ); 54 | }); 55 | 56 | it('throws an error if a given variant Id is not found in the cart', async () => { 57 | const {result} = await renderHookWithContext(() => 58 | useRemoveItemsFromCart(), 59 | ); 60 | 61 | // @ts-ignore 62 | await expect( 63 | result.current([Mocks.VARIANT_ID_IN_CART, 'some_bogus_id']), 64 | ).rejects.toThrow( 65 | 'Could not find line item in cart with variant id: some_bogus_id', 66 | ); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useSetCartUnsafe.test.ts: -------------------------------------------------------------------------------- 1 | import {renderHook, act} from '@testing-library/react-hooks'; 2 | import {Mocks, renderHookWithContext} from '../../mocks'; 3 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 4 | import {useSetCartUnsafe} from '../useSetCartUnsafe'; 5 | 6 | describe('useSetCartUnsafe()', () => { 7 | it('returns a function to set the cart in state', async () => { 8 | const localStorageSpy = jest.spyOn(LocalStorage, 'set'); 9 | const {result} = await renderHookWithContext(() => useSetCartUnsafe()); 10 | 11 | const newCart = {...Mocks.CART, id: 'my_new_cart'}; 12 | act(() => { 13 | // @ts-ignore 14 | result.current(newCart); 15 | }); 16 | 17 | expect(localStorageSpy).toHaveBeenCalledWith( 18 | LocalStorageKeys.CART, 19 | JSON.stringify(newCart), 20 | ); 21 | }); 22 | 23 | it('throws an error if a given variant Id is not found in the cart', async () => { 24 | const {result} = renderHook(() => useSetCartUnsafe()); 25 | 26 | // @ts-ignore 27 | expect(() => result.current(Mocks.CART)).toThrow( 28 | 'You forgot to wrap this in a Provider object', 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/__tests__/useUpdateItemQuantity.test.ts: -------------------------------------------------------------------------------- 1 | import {act} from '@testing-library/react-hooks'; 2 | import {Mocks, getCurrentCart, renderHookWithContext} from '../../mocks'; 3 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 4 | import {useUpdateItemQuantity} from '../useUpdateItemQuantity'; 5 | 6 | afterEach(() => { 7 | LocalStorage.set(LocalStorageKeys.CART, ''); 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | describe('useUpdateItemQuantity()', () => { 12 | it('updates the quantity for an item given a variant id', async () => { 13 | const newQuantity = 2; 14 | const {result, waitForNextUpdate} = await renderHookWithContext(() => 15 | useUpdateItemQuantity(), 16 | ); 17 | 18 | act(() => { 19 | result.current(Mocks.VARIANT_ID_IN_CART, newQuantity); 20 | }); 21 | await waitForNextUpdate(); 22 | 23 | const cart = getCurrentCart(); 24 | const item = cart.lineItems.find( 25 | // @ts-ignore 26 | (item) => item.variant.id === Mocks.VARIANT_ID_IN_CART, 27 | ); 28 | 29 | expect(item!.quantity).toBe(newQuantity); 30 | }); 31 | 32 | it('throws an error if no variantId is provided', async () => { 33 | const {result} = await renderHookWithContext(() => useUpdateItemQuantity()); 34 | 35 | // @ts-ignore 36 | await expect(result.current(null, 2)).rejects.toThrow( 37 | 'Must provide a variant id', 38 | ); 39 | }); 40 | 41 | it('throws an error if no quantity is provided', async () => { 42 | const {result} = await renderHookWithContext(() => useUpdateItemQuantity()); 43 | 44 | await expect( 45 | // @ts-ignore 46 | result.current(Mocks.VARIANT_ID_IN_CART, null), 47 | ).rejects.toThrow('Quantity must be greater than 0'); 48 | }); 49 | 50 | it('throws an error if quantity less than 0 is provided', async () => { 51 | const {result} = await renderHookWithContext(() => useUpdateItemQuantity()); 52 | 53 | await expect(result.current(Mocks.VARIANT_ID_IN_CART, -1)).rejects.toThrow( 54 | 'Quantity must be greater than 0', 55 | ); 56 | }); 57 | 58 | it('throws an error if a variant id is provided that cannot be found', async () => { 59 | const {result} = await renderHookWithContext(() => useUpdateItemQuantity()); 60 | 61 | await expect(result.current('some_fake_id', 2)).rejects.toThrow( 62 | 'Item with variantId some_fake_id not in cart', 63 | ); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export {useClientUnsafe} from './useClientUnsafe'; 2 | export {useSetCartUnsafe} from './useSetCartUnsafe'; 3 | export {useCart} from './useCart'; 4 | export {useCartCount} from './useCartCount'; 5 | export {useAddItemToCart} from './useAddItemToCart'; 6 | export {useAddItemsToCart} from './useAddItemsToCart'; 7 | export {useRemoveItemFromCart} from './useRemoveItemFromCart'; 8 | export {useRemoveItemsFromCart} from './useRemoveItemsFromCart'; 9 | export {useCartItems} from './useCartItems'; 10 | export {useCheckoutUrl} from './useCheckoutUrl'; 11 | export {useGetLineItem} from './useGetLineItem'; 12 | export {useUpdateItemQuantity} from './useUpdateItemQuantity'; 13 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useAddItemToCart.ts: -------------------------------------------------------------------------------- 1 | import {useAddItemsToCart} from './useAddItemsToCart'; 2 | import {AttributeInput} from '../types'; 3 | 4 | export function useAddItemToCart() { 5 | const addItemsToCart = useAddItemsToCart(); 6 | 7 | async function addItemToCart( 8 | variantId: number | string, 9 | quantity: number, 10 | customAttributes?: AttributeInput[], 11 | ) { 12 | const item = [{variantId, quantity, customAttributes}]; 13 | 14 | return addItemsToCart(item); 15 | } 16 | 17 | return addItemToCart; 18 | } 19 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useAddItemsToCart.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | import ShopifyBuy from 'shopify-buy'; 4 | import {LineItemPatch} from '../types'; 5 | 6 | export function useAddItemsToCart() { 7 | const {client, cart, setCart} = useContext(Context); 8 | 9 | async function addItemsToCart(items: LineItemPatch[]) { 10 | if (cart == null || client == null) { 11 | throw new Error('Called addItemsToCart too soon'); 12 | } 13 | 14 | if (items.length < 1) { 15 | throw new Error( 16 | 'Must include at least one line item, empty line items found', 17 | ); 18 | } 19 | 20 | items.forEach((item) => { 21 | if (item.variantId == null) { 22 | throw new Error(`Missing variantId in item`); 23 | } 24 | 25 | if (item.quantity == null) { 26 | throw new Error( 27 | `Missing quantity in item with variant id: ${item.variantId}`, 28 | ); 29 | } else if (typeof item.quantity != 'number') { 30 | throw new Error( 31 | `Quantity is not a number in item with variant id: ${item.variantId}`, 32 | ); 33 | } else if (item.quantity < 1) { 34 | throw new Error( 35 | `Quantity must not be less than one in item with variant id: ${item.variantId}`, 36 | ); 37 | } 38 | }); 39 | 40 | const newCart = await client.checkout.addLineItems( 41 | cart.id, 42 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 43 | // @ts-ignore 44 | items as ShopifyBuy.LineItem[], 45 | ); 46 | setCart(newCart); 47 | } 48 | 49 | return addItemsToCart; 50 | } 51 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useCart.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | 4 | export function useCart() { 5 | const {cart} = useContext(Context); 6 | return cart; 7 | } 8 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useCartCount.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | 4 | export function useCartCount() { 5 | const {cart} = useContext(Context); 6 | if (cart == null || cart.lineItems.length < 1) { 7 | return 0; 8 | } 9 | 10 | const count = cart.lineItems.reduce((totalCount, lineItem) => { 11 | return totalCount + lineItem.quantity; 12 | }, 0); 13 | 14 | return count; 15 | } 16 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useCartItems.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | 4 | export function useCartItems() { 5 | const {cart} = useContext(Context); 6 | if (cart == null || cart.lineItems == null) { 7 | return []; 8 | } 9 | 10 | return cart.lineItems; 11 | } 12 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useCheckoutUrl.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | 4 | export function useCheckoutUrl(): string | null { 5 | const {cart} = useContext(Context); 6 | if (cart == null) { 7 | return null; 8 | } 9 | 10 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 11 | // @ts-ignore 12 | return cart.webUrl; 13 | } 14 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useClientUnsafe.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | 4 | export function useClientUnsafe() { 5 | const {client} = useContext(Context); 6 | return client; 7 | } 8 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useGetLineItem.ts: -------------------------------------------------------------------------------- 1 | import {useCartItems} from './useCartItems'; 2 | 3 | export function useGetLineItem() { 4 | const cartItems = useCartItems(); 5 | 6 | function getLineItem(variantId: string | number): ShopifyBuy.LineItem | null { 7 | if (cartItems.length < 1) { 8 | return null; 9 | } 10 | 11 | const item = cartItems.find((cartItem) => { 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 13 | // @ts-ignore 14 | return cartItem.variant.id === variantId; 15 | }); 16 | 17 | if (item == null) { 18 | return null; 19 | } 20 | 21 | return item; 22 | } 23 | 24 | return getLineItem; 25 | } 26 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useRemoveItemFromCart.ts: -------------------------------------------------------------------------------- 1 | import {useRemoveItemsFromCart} from './useRemoveItemsFromCart'; 2 | 3 | export function useRemoveItemFromCart() { 4 | const removeItemsFromCart = useRemoveItemsFromCart(); 5 | 6 | async function removeItemFromCart(variantId: number | string) { 7 | if (variantId === '' || variantId == null) { 8 | throw new Error('VariantId must not be blank or null'); 9 | } 10 | 11 | return removeItemsFromCart([String(variantId)]); 12 | } 13 | 14 | return removeItemFromCart; 15 | } 16 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useRemoveItemsFromCart.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | import {useGetLineItem} from './useGetLineItem'; 4 | 5 | export function useRemoveItemsFromCart() { 6 | const {client, cart, setCart} = useContext(Context); 7 | const getLineItem = useGetLineItem(); 8 | 9 | async function removeItemsFromCart(variantIds: string[]) { 10 | if (cart == null || client == null) { 11 | throw new Error('Called removeItemsFromCart too soon'); 12 | } 13 | 14 | if (variantIds.length < 1) { 15 | throw new Error('Must include at least one item to remove'); 16 | } 17 | 18 | const lineItemIds = variantIds.map((variantId) => { 19 | const lineItem = getLineItem(variantId); 20 | if (lineItem === null) { 21 | throw new Error( 22 | `Could not find line item in cart with variant id: ${variantId}`, 23 | ); 24 | } 25 | return String(lineItem.id); 26 | }); 27 | 28 | const newCart = await client.checkout.removeLineItems(cart.id, lineItemIds); 29 | setCart(newCart); 30 | } 31 | 32 | return removeItemsFromCart; 33 | } 34 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useSetCartUnsafe.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | 4 | export function useSetCartUnsafe() { 5 | const {setCart} = useContext(Context); 6 | return setCart; 7 | } 8 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/hooks/useUpdateItemQuantity.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {Context} from '../Context'; 3 | 4 | import {useGetLineItem} from './useGetLineItem'; 5 | 6 | export function useUpdateItemQuantity() { 7 | const {client, cart, setCart} = useContext(Context); 8 | const getLineItem = useGetLineItem(); 9 | 10 | async function updateItemQuantity( 11 | variantId: string | number, 12 | quantity: number, 13 | ) { 14 | if (variantId == null) { 15 | throw new Error('Must provide a variant id'); 16 | } 17 | 18 | if (quantity == null || Number(quantity) < 0) { 19 | throw new Error('Quantity must be greater than 0'); 20 | } 21 | 22 | const lineItem = getLineItem(variantId); 23 | if (lineItem == null) { 24 | throw new Error(`Item with variantId ${variantId} not in cart`); 25 | } 26 | 27 | // eslint-disable-next-line @typescript-eslint/ban-ts-ignore 28 | // @ts-ignore 29 | const newCart = await client.checkout.updateLineItems(cart.id, [ 30 | {id: lineItem.id, quantity}, 31 | ]); 32 | setCart(newCart); 33 | } 34 | 35 | return updateItemQuantity; 36 | } 37 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/index.ts: -------------------------------------------------------------------------------- 1 | export {ContextProvider} from './ContextProvider'; 2 | export * from './hooks'; 3 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/cart.ts: -------------------------------------------------------------------------------- 1 | export const VARIANT_ID_IN_CART = 2 | 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTA5NTcyNA=='; 3 | 4 | export const CHECKOUT_URL = 5 | 'https://only-down.myshopify.com/30073356332/checkouts/4d395496fb27ee8c53b08029f9880ba3?key=13fc7660aeb709de1baa5f4b1bf7094c'; 6 | 7 | export const CART = { 8 | id: 9 | 'Z2lkOi8vc2hvcGlmeS9DaGVja291dC80ZDM5NTQ5NmZiMjdlZThjNTNiMDgwMjlmOTg4MGJhMz9rZXk9MTNmYzc2NjBhZWI3MDlkZTFiYWE1ZjRiMWJmNzA5NGM=', 10 | ready: true, 11 | requiresShipping: true, 12 | note: null, 13 | paymentDue: '120.00', 14 | paymentDueV2: { 15 | amount: '120.0', 16 | currencyCode: 'USD', 17 | type: { 18 | name: 'MoneyV2', 19 | kind: 'OBJECT', 20 | fieldBaseTypes: {amount: 'Decimal', currencyCode: 'CurrencyCode'}, 21 | implementsNode: false, 22 | }, 23 | }, 24 | webUrl: CHECKOUT_URL, 25 | orderStatusUrl: null, 26 | taxExempt: false, 27 | taxesIncluded: false, 28 | currencyCode: 'USD', 29 | totalTax: '0.00', 30 | totalTaxV2: { 31 | amount: '0.0', 32 | currencyCode: 'USD', 33 | type: { 34 | name: 'MoneyV2', 35 | kind: 'OBJECT', 36 | fieldBaseTypes: {amount: 'Decimal', currencyCode: 'CurrencyCode'}, 37 | implementsNode: false, 38 | }, 39 | }, 40 | lineItemsSubtotalPrice: { 41 | amount: '120.0', 42 | currencyCode: 'USD', 43 | type: { 44 | name: 'MoneyV2', 45 | kind: 'OBJECT', 46 | fieldBaseTypes: {amount: 'Decimal', currencyCode: 'CurrencyCode'}, 47 | implementsNode: false, 48 | }, 49 | }, 50 | subtotalPrice: '120.00', 51 | subtotalPriceV2: { 52 | amount: '120.0', 53 | currencyCode: 'USD', 54 | type: { 55 | name: 'MoneyV2', 56 | kind: 'OBJECT', 57 | fieldBaseTypes: {amount: 'Decimal', currencyCode: 'CurrencyCode'}, 58 | implementsNode: false, 59 | }, 60 | }, 61 | totalPrice: '120.00', 62 | totalPriceV2: { 63 | amount: '120.0', 64 | currencyCode: 'USD', 65 | type: { 66 | name: 'MoneyV2', 67 | kind: 'OBJECT', 68 | fieldBaseTypes: {amount: 'Decimal', currencyCode: 'CurrencyCode'}, 69 | implementsNode: false, 70 | }, 71 | }, 72 | completedAt: null, 73 | createdAt: '2020-02-29T20:23:04Z', 74 | updatedAt: '2020-03-04T01:23:20Z', 75 | email: null, 76 | discountApplications: [], 77 | appliedGiftCards: [], 78 | shippingAddress: null, 79 | shippingLine: null, 80 | customAttributes: [], 81 | order: null, 82 | lineItems: [ 83 | { 84 | id: 85 | 'Z2lkOi8vc2hvcGlmeS9DaGVja291dExpbmVJdGVtLzMxNzg0NDgxMDk1NzI0MD9jaGVja291dD00ZDM5NTQ5NmZiMjdlZThjNTNiMDgwMjlmOTg4MGJhMw==', 86 | title: "Men's Down Jacket", 87 | variant: { 88 | id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTA5NTcyNA==', 89 | title: 'Blue / Small', 90 | price: '50.00', 91 | priceV2: { 92 | amount: '50.0', 93 | currencyCode: 'USD', 94 | type: { 95 | name: 'MoneyV2', 96 | kind: 'OBJECT', 97 | fieldBaseTypes: {amount: 'Decimal', currencyCode: 'CurrencyCode'}, 98 | implementsNode: false, 99 | }, 100 | }, 101 | presentmentPrices: [ 102 | { 103 | price: { 104 | amount: '50.0', 105 | currencyCode: 'USD', 106 | type: { 107 | name: 'MoneyV2', 108 | kind: 'OBJECT', 109 | fieldBaseTypes: { 110 | amount: 'Decimal', 111 | currencyCode: 'CurrencyCode', 112 | }, 113 | implementsNode: false, 114 | }, 115 | }, 116 | compareAtPrice: null, 117 | type: { 118 | name: 'ProductVariantPricePair', 119 | kind: 'OBJECT', 120 | fieldBaseTypes: {compareAtPrice: 'MoneyV2', price: 'MoneyV2'}, 121 | implementsNode: false, 122 | }, 123 | hasNextPage: false, 124 | hasPreviousPage: false, 125 | variableValues: { 126 | checkoutId: 127 | 'Z2lkOi8vc2hvcGlmeS9DaGVja291dC80ZDM5NTQ5NmZiMjdlZThjNTNiMDgwMjlmOTg4MGJhMz9rZXk9MTNmYzc2NjBhZWI3MDlkZTFiYWE1ZjRiMWJmNzA5NGM=', 128 | lineItems: [ 129 | { 130 | variantId: 131 | 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTA5NTcyNA==', 132 | quantity: 1, 133 | }, 134 | ], 135 | }, 136 | }, 137 | ], 138 | weight: 4, 139 | available: true, 140 | sku: '', 141 | compareAtPrice: null, 142 | compareAtPriceV2: null, 143 | image: { 144 | id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTM4NjcxNjM0ODQyMDQ=', 145 | src: 146 | 'https://cdn.shopify.com/s/files/1/0300/7335/6332/products/down-jacket-blue.jpg?v=1580011570', 147 | altText: null, 148 | type: { 149 | name: 'Image', 150 | kind: 'OBJECT', 151 | fieldBaseTypes: { 152 | altText: 'String', 153 | id: 'ID', 154 | originalSrc: 'URL', 155 | src: 'URL', 156 | }, 157 | implementsNode: false, 158 | }, 159 | }, 160 | selectedOptions: [ 161 | { 162 | name: 'Color', 163 | value: 'Blue', 164 | type: { 165 | name: 'SelectedOption', 166 | kind: 'OBJECT', 167 | fieldBaseTypes: {name: 'String', value: 'String'}, 168 | implementsNode: false, 169 | }, 170 | }, 171 | { 172 | name: 'Size', 173 | value: 'Small', 174 | type: { 175 | name: 'SelectedOption', 176 | kind: 'OBJECT', 177 | fieldBaseTypes: {name: 'String', value: 'String'}, 178 | implementsNode: false, 179 | }, 180 | }, 181 | ], 182 | unitPrice: null, 183 | unitPriceMeasurement: { 184 | measuredType: null, 185 | quantityUnit: null, 186 | quantityValue: 0, 187 | referenceUnit: null, 188 | referenceValue: 0, 189 | type: { 190 | name: 'UnitPriceMeasurement', 191 | kind: 'OBJECT', 192 | fieldBaseTypes: { 193 | measuredType: 'UnitPriceMeasurementMeasuredType', 194 | quantityUnit: 'UnitPriceMeasurementMeasuredUnit', 195 | quantityValue: 'Float', 196 | referenceUnit: 'UnitPriceMeasurementMeasuredUnit', 197 | referenceValue: 'Int', 198 | }, 199 | implementsNode: false, 200 | }, 201 | }, 202 | product: { 203 | id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzQ0NTc0Mzc2MjY0MTI=', 204 | handle: 'mens-down-jacket', 205 | type: { 206 | name: 'Product', 207 | kind: 'OBJECT', 208 | fieldBaseTypes: { 209 | availableForSale: 'Boolean', 210 | createdAt: 'DateTime', 211 | description: 'String', 212 | descriptionHtml: 'HTML', 213 | handle: 'String', 214 | id: 'ID', 215 | images: 'ImageConnection', 216 | onlineStoreUrl: 'URL', 217 | options: 'ProductOption', 218 | productType: 'String', 219 | publishedAt: 'DateTime', 220 | title: 'String', 221 | updatedAt: 'DateTime', 222 | variants: 'ProductVariantConnection', 223 | vendor: 'String', 224 | }, 225 | implementsNode: true, 226 | }, 227 | }, 228 | type: { 229 | name: 'ProductVariant', 230 | kind: 'OBJECT', 231 | fieldBaseTypes: { 232 | availableForSale: 'Boolean', 233 | compareAtPrice: 'Money', 234 | compareAtPriceV2: 'MoneyV2', 235 | id: 'ID', 236 | image: 'Image', 237 | presentmentPrices: 'ProductVariantPricePairConnection', 238 | price: 'Money', 239 | priceV2: 'MoneyV2', 240 | product: 'Product', 241 | selectedOptions: 'SelectedOption', 242 | sku: 'String', 243 | title: 'String', 244 | unitPrice: 'MoneyV2', 245 | unitPriceMeasurement: 'UnitPriceMeasurement', 246 | weight: 'Float', 247 | }, 248 | implementsNode: true, 249 | }, 250 | }, 251 | quantity: 1, 252 | customAttributes: [], 253 | discountAllocations: [], 254 | type: { 255 | name: 'CheckoutLineItem', 256 | kind: 'OBJECT', 257 | fieldBaseTypes: { 258 | customAttributes: 'Attribute', 259 | discountAllocations: 'DiscountAllocation', 260 | id: 'ID', 261 | quantity: 'Int', 262 | title: 'String', 263 | variant: 'ProductVariant', 264 | }, 265 | implementsNode: true, 266 | }, 267 | hasNextPage: {value: true}, 268 | hasPreviousPage: false, 269 | variableValues: { 270 | checkoutId: 271 | 'Z2lkOi8vc2hvcGlmeS9DaGVja291dC80ZDM5NTQ5NmZiMjdlZThjNTNiMDgwMjlmOTg4MGJhMz9rZXk9MTNmYzc2NjBhZWI3MDlkZTFiYWE1ZjRiMWJmNzA5NGM=', 272 | lineItems: [ 273 | { 274 | variantId: 275 | 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTA5NTcyNA==', 276 | quantity: 1, 277 | }, 278 | ], 279 | }, 280 | }, 281 | { 282 | id: 283 | 'Z2lkOi8vc2hvcGlmeS9DaGVja291dExpbmVJdGVtLzMxNzg0NTQzMDYwMDEyMD9jaGVja291dD00ZDM5NTQ5NmZiMjdlZThjNTNiMDgwMjlmOTg4MGJhMw==', 284 | title: "Women's Down Vest", 285 | variant: { 286 | id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDU0MzA2MDAxMg==', 287 | title: 'Black / Small', 288 | price: '35.00', 289 | priceV2: { 290 | amount: '35.0', 291 | currencyCode: 'USD', 292 | type: { 293 | name: 'MoneyV2', 294 | kind: 'OBJECT', 295 | fieldBaseTypes: {amount: 'Decimal', currencyCode: 'CurrencyCode'}, 296 | implementsNode: false, 297 | }, 298 | }, 299 | presentmentPrices: [ 300 | { 301 | price: { 302 | amount: '35.0', 303 | currencyCode: 'USD', 304 | type: { 305 | name: 'MoneyV2', 306 | kind: 'OBJECT', 307 | fieldBaseTypes: { 308 | amount: 'Decimal', 309 | currencyCode: 'CurrencyCode', 310 | }, 311 | implementsNode: false, 312 | }, 313 | }, 314 | compareAtPrice: null, 315 | type: { 316 | name: 'ProductVariantPricePair', 317 | kind: 'OBJECT', 318 | fieldBaseTypes: {compareAtPrice: 'MoneyV2', price: 'MoneyV2'}, 319 | implementsNode: false, 320 | }, 321 | hasNextPage: false, 322 | hasPreviousPage: false, 323 | variableValues: { 324 | checkoutId: 325 | 'Z2lkOi8vc2hvcGlmeS9DaGVja291dC80ZDM5NTQ5NmZiMjdlZThjNTNiMDgwMjlmOTg4MGJhMz9rZXk9MTNmYzc2NjBhZWI3MDlkZTFiYWE1ZjRiMWJmNzA5NGM=', 326 | lineItems: [ 327 | { 328 | variantId: 329 | 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTA5NTcyNA==', 330 | quantity: 1, 331 | }, 332 | ], 333 | }, 334 | }, 335 | ], 336 | weight: 0, 337 | available: true, 338 | sku: '', 339 | compareAtPrice: null, 340 | compareAtPriceV2: null, 341 | image: { 342 | id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0SW1hZ2UvMTM4NjcyMjIwNDA2MjA=', 343 | src: 344 | 'https://cdn.shopify.com/s/files/1/0300/7335/6332/products/womens-vest-black.jpg?v=1580011549', 345 | altText: null, 346 | type: { 347 | name: 'Image', 348 | kind: 'OBJECT', 349 | fieldBaseTypes: { 350 | altText: 'String', 351 | id: 'ID', 352 | originalSrc: 'URL', 353 | src: 'URL', 354 | }, 355 | implementsNode: false, 356 | }, 357 | }, 358 | selectedOptions: [ 359 | { 360 | name: 'Color', 361 | value: 'Black', 362 | type: { 363 | name: 'SelectedOption', 364 | kind: 'OBJECT', 365 | fieldBaseTypes: {name: 'String', value: 'String'}, 366 | implementsNode: false, 367 | }, 368 | }, 369 | { 370 | name: 'Size', 371 | value: 'Small', 372 | type: { 373 | name: 'SelectedOption', 374 | kind: 'OBJECT', 375 | fieldBaseTypes: {name: 'String', value: 'String'}, 376 | implementsNode: false, 377 | }, 378 | }, 379 | ], 380 | unitPrice: null, 381 | unitPriceMeasurement: { 382 | measuredType: null, 383 | quantityUnit: null, 384 | quantityValue: 0, 385 | referenceUnit: null, 386 | referenceValue: 0, 387 | type: { 388 | name: 'UnitPriceMeasurement', 389 | kind: 'OBJECT', 390 | fieldBaseTypes: { 391 | measuredType: 'UnitPriceMeasurementMeasuredType', 392 | quantityUnit: 'UnitPriceMeasurementMeasuredUnit', 393 | quantityValue: 'Float', 394 | referenceUnit: 'UnitPriceMeasurementMeasuredUnit', 395 | referenceValue: 'Int', 396 | }, 397 | implementsNode: false, 398 | }, 399 | }, 400 | product: { 401 | id: 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0LzQ0NTc0NTc3NDU5NjQ=', 402 | handle: 'womens-down-vest', 403 | type: { 404 | name: 'Product', 405 | kind: 'OBJECT', 406 | fieldBaseTypes: { 407 | availableForSale: 'Boolean', 408 | createdAt: 'DateTime', 409 | description: 'String', 410 | descriptionHtml: 'HTML', 411 | handle: 'String', 412 | id: 'ID', 413 | images: 'ImageConnection', 414 | onlineStoreUrl: 'URL', 415 | options: 'ProductOption', 416 | productType: 'String', 417 | publishedAt: 'DateTime', 418 | title: 'String', 419 | updatedAt: 'DateTime', 420 | variants: 'ProductVariantConnection', 421 | vendor: 'String', 422 | }, 423 | implementsNode: true, 424 | }, 425 | }, 426 | type: { 427 | name: 'ProductVariant', 428 | kind: 'OBJECT', 429 | fieldBaseTypes: { 430 | availableForSale: 'Boolean', 431 | compareAtPrice: 'Money', 432 | compareAtPriceV2: 'MoneyV2', 433 | id: 'ID', 434 | image: 'Image', 435 | presentmentPrices: 'ProductVariantPricePairConnection', 436 | price: 'Money', 437 | priceV2: 'MoneyV2', 438 | product: 'Product', 439 | selectedOptions: 'SelectedOption', 440 | sku: 'String', 441 | title: 'String', 442 | unitPrice: 'MoneyV2', 443 | unitPriceMeasurement: 'UnitPriceMeasurement', 444 | weight: 'Float', 445 | }, 446 | implementsNode: true, 447 | }, 448 | }, 449 | quantity: 2, 450 | customAttributes: [], 451 | discountAllocations: [], 452 | type: { 453 | name: 'CheckoutLineItem', 454 | kind: 'OBJECT', 455 | fieldBaseTypes: { 456 | customAttributes: 'Attribute', 457 | discountAllocations: 'DiscountAllocation', 458 | id: 'ID', 459 | quantity: 'Int', 460 | title: 'String', 461 | variant: 'ProductVariant', 462 | }, 463 | implementsNode: true, 464 | }, 465 | hasNextPage: false, 466 | hasPreviousPage: {value: true}, 467 | variableValues: { 468 | checkoutId: 469 | 'Z2lkOi8vc2hvcGlmeS9DaGVja291dC80ZDM5NTQ5NmZiMjdlZThjNTNiMDgwMjlmOTg4MGJhMz9rZXk9MTNmYzc2NjBhZWI3MDlkZTFiYWE1ZjRiMWJmNzA5NGM=', 470 | lineItems: [ 471 | { 472 | variantId: 473 | 'Z2lkOi8vc2hvcGlmeS9Qcm9kdWN0VmFyaWFudC8zMTc4NDQ4MTA5NTcyNA==', 474 | quantity: 1, 475 | }, 476 | ], 477 | }, 478 | }, 479 | ], 480 | type: { 481 | name: 'Checkout', 482 | kind: 'OBJECT', 483 | fieldBaseTypes: { 484 | appliedGiftCards: 'AppliedGiftCard', 485 | completedAt: 'DateTime', 486 | createdAt: 'DateTime', 487 | currencyCode: 'CurrencyCode', 488 | customAttributes: 'Attribute', 489 | discountApplications: 'DiscountApplicationConnection', 490 | email: 'String', 491 | id: 'ID', 492 | lineItems: 'CheckoutLineItemConnection', 493 | lineItemsSubtotalPrice: 'MoneyV2', 494 | note: 'String', 495 | order: 'Order', 496 | orderStatusUrl: 'URL', 497 | paymentDue: 'Money', 498 | paymentDueV2: 'MoneyV2', 499 | ready: 'Boolean', 500 | requiresShipping: 'Boolean', 501 | shippingAddress: 'MailingAddress', 502 | shippingLine: 'ShippingRate', 503 | subtotalPrice: 'Money', 504 | subtotalPriceV2: 'MoneyV2', 505 | taxExempt: 'Boolean', 506 | taxesIncluded: 'Boolean', 507 | totalPrice: 'Money', 508 | totalPriceV2: 'MoneyV2', 509 | totalTax: 'Money', 510 | totalTaxV2: 'MoneyV2', 511 | updatedAt: 'DateTime', 512 | webUrl: 'URL', 513 | }, 514 | implementsNode: true, 515 | }, 516 | userErrors: [], 517 | }; 518 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/client.ts: -------------------------------------------------------------------------------- 1 | import {CART} from './cart'; 2 | 3 | export const CLIENT = { 4 | product: {}, 5 | collection: {}, 6 | checkout: { 7 | create: jest.fn(() => CART), 8 | fetch: jest.fn(() => CART), 9 | addLineItems: jest.fn((cartId, items) => { 10 | return {...CART, lineItems: [...CART.lineItems, ...items]}; 11 | }), 12 | clearLineItems: jest.fn(), 13 | addVariants: jest.fn(), 14 | removeLineItems: jest.fn((_, lineItemIds) => { 15 | const newLineItems = CART.lineItems 16 | .map((lineItem) => { 17 | if (lineItemIds.includes(lineItem.id)) { 18 | return null; 19 | } 20 | return lineItem; 21 | }) 22 | .filter(Boolean); 23 | 24 | return {...CART, lineItems: newLineItems}; 25 | }), 26 | updateLineItems: jest.fn((_, itemsToUpdate) => { 27 | const lineItems = CART.lineItems.map((lineItem) => { 28 | let newLineItem = lineItem; 29 | itemsToUpdate.forEach((item: {id: string; quantity: number}) => { 30 | if (item.id === lineItem.id) { 31 | newLineItem = { 32 | ...lineItem, 33 | quantity: item.quantity, 34 | }; 35 | } 36 | }); 37 | 38 | return newLineItem; 39 | }); 40 | 41 | return {...CART, lineItems: lineItems}; 42 | }), 43 | }, 44 | shop: {}, 45 | image: {}, 46 | fetchNextPage: jest.fn(), 47 | }; 48 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/constants.ts: -------------------------------------------------------------------------------- 1 | export const ACCESS_TOKEN = 'access123'; 2 | export const SHOP_NAME = 'some-shop'; 3 | export const SHOP_NAME_WITH_CUSTOM_DOMAIN = 'some-shop.com'; 4 | export const MYSHOPIFY_DOMAIN = `${SHOP_NAME}.myshopify.com`; 5 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/contextWrappers/index.ts: -------------------------------------------------------------------------------- 1 | export {wrapWithContext} from './wrapWithContext'; 2 | export {renderWithContext} from './renderWithContext'; 3 | export {renderHookWithContext} from './renderHookWithContext'; 4 | export {renderHookWithContextSynchronously} from './renderHookWithContextSynchronously'; 5 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/contextWrappers/renderHookWithContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | renderHook, 3 | RenderHookOptions, 4 | RenderHookResult, 5 | } from '@testing-library/react-hooks'; 6 | import {wrapWithContext} from './wrapWithContext'; 7 | import {ContextOptions} from './types'; 8 | 9 | // this type signature matches renderHook's type signature 10 | export async function renderHookWithContext( 11 | callback: (props: P) => R, 12 | contextOptions?: Partial, 13 | renderHookOptions?: RenderHookOptions

, 14 | ): Promise> { 15 | const hookRender = renderHook(callback, { 16 | ...renderHookOptions, 17 | wrapper: wrapWithContext(contextOptions), 18 | }); 19 | await hookRender.waitForNextUpdate(); 20 | return hookRender; 21 | } 22 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/contextWrappers/renderHookWithContextSynchronously.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | renderHook, 3 | RenderHookOptions, 4 | RenderHookResult, 5 | } from '@testing-library/react-hooks'; 6 | import {wrapWithContext} from './wrapWithContext'; 7 | import {ContextOptions} from './types'; 8 | 9 | // this type signature matches renderHook's type signature 10 | export function renderHookWithContextSynchronously( 11 | callback: (props: P) => R, 12 | contextOptions?: Partial, 13 | renderHookOptions?: RenderHookOptions

, 14 | ): RenderHookResult { 15 | return renderHook(callback, { 16 | ...renderHookOptions, 17 | wrapper: wrapWithContext(contextOptions), 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/contextWrappers/renderWithContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from '@testing-library/react'; 3 | import {ContextOptions} from './types'; 4 | import {wrapWithContext} from './wrapWithContext'; 5 | 6 | export function renderWithContext( 7 | component: React.ReactNode, 8 | givenOptions?: Partial, 9 | ) { 10 | const wrapperFunction = wrapWithContext(givenOptions); 11 | return render(wrapperFunction({children: component})); 12 | } 13 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/contextWrappers/types.ts: -------------------------------------------------------------------------------- 1 | export interface ContextOptions { 2 | shopName: string; 3 | accessToken: string; 4 | shouldSetInitialCart: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/contextWrappers/wrapWithContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ContextProvider} from '../../ContextProvider'; 3 | import {MYSHOPIFY_DOMAIN, ACCESS_TOKEN} from '../constants'; 4 | import {CART} from '../cart'; 5 | import {ContextOptions} from './types'; 6 | import {LocalStorage, LocalStorageKeys} from '../../utils'; 7 | 8 | export function wrapWithContext(givenOptions?: Partial) { 9 | const defaults = { 10 | shopName: MYSHOPIFY_DOMAIN, 11 | accessToken: ACCESS_TOKEN, 12 | shouldSetInitialCart: true, 13 | }; 14 | 15 | const options = Object.assign({}, defaults, givenOptions); 16 | 17 | if (options.shouldSetInitialCart) { 18 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(CART)); 19 | } 20 | 21 | const wrapperFunction = ({children}: {children?: React.ReactNode}) => ( 22 | 26 | {children} 27 | 28 | ); 29 | 30 | return wrapperFunction; 31 | } 32 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/emptyCart.ts: -------------------------------------------------------------------------------- 1 | import {CART} from './cart'; 2 | 3 | export const EMPTY_CART = { 4 | ...CART, 5 | lineItems: [], 6 | }; 7 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/getCurrentCart.ts: -------------------------------------------------------------------------------- 1 | import {LocalStorage, LocalStorageKeys} from '../utils'; 2 | import ShopifyBuy from 'shopify-buy'; 3 | 4 | export function getCurrentCart(): ShopifyBuy.Cart { 5 | return JSON.parse( 6 | LocalStorage.get(LocalStorageKeys.CART) || '', 7 | ) as ShopifyBuy.Cart; 8 | } 9 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | import {CART, VARIANT_ID_IN_CART, CHECKOUT_URL} from './cart'; 2 | import {EMPTY_CART} from './emptyCart'; 3 | import {PURCHASED_CART} from './purchasedCart'; 4 | import {CLIENT} from './client'; 5 | import { 6 | ACCESS_TOKEN, 7 | SHOP_NAME, 8 | SHOP_NAME_WITH_CUSTOM_DOMAIN, 9 | MYSHOPIFY_DOMAIN, 10 | } from './constants'; 11 | import { 12 | wrapWithContext, 13 | renderWithContext, 14 | renderHookWithContext, 15 | renderHookWithContextSynchronously, 16 | } from './contextWrappers'; 17 | import {getCurrentCart} from './getCurrentCart'; 18 | 19 | const Mocks = { 20 | CART, 21 | EMPTY_CART, 22 | PURCHASED_CART, 23 | CLIENT, 24 | ACCESS_TOKEN, 25 | SHOP_NAME, 26 | SHOP_NAME_WITH_CUSTOM_DOMAIN, 27 | MYSHOPIFY_DOMAIN, 28 | VARIANT_ID_IN_CART, 29 | CHECKOUT_URL, 30 | }; 31 | 32 | export { 33 | Mocks, 34 | wrapWithContext, 35 | getCurrentCart, 36 | renderWithContext, 37 | renderHookWithContext, 38 | renderHookWithContextSynchronously, 39 | }; 40 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/mocks/purchasedCart.ts: -------------------------------------------------------------------------------- 1 | import {CART} from './cart'; 2 | 3 | export const PURCHASED_CART = { 4 | ...CART, 5 | completedAt: '2020-03-13T20:23:04Z', 6 | }; 7 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface AttributeInput { 2 | [key: string]: string; 3 | } 4 | 5 | export interface LineItemPatch { 6 | variantId: string | number; 7 | quantity: number; 8 | customAttributes?: AttributeInput[]; 9 | } 10 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/LocalStorage/LocalStorage.ts: -------------------------------------------------------------------------------- 1 | import ShopifyBuy from 'shopify-buy'; 2 | import {LocalStorageKeys} from './keys'; 3 | import {isCart} from '../../utils'; 4 | 5 | function set(key: string, value: string) { 6 | const isBrowser = typeof window !== 'undefined'; 7 | if (isBrowser) { 8 | window.localStorage.setItem(key, value); 9 | } 10 | } 11 | 12 | function get(key: string) { 13 | const isBrowser = typeof window !== 'undefined'; 14 | if (!isBrowser) { 15 | return null; 16 | } 17 | 18 | try { 19 | const item = window.localStorage.getItem(key); 20 | return item; 21 | } catch { 22 | return null; 23 | } 24 | } 25 | 26 | function getInitialCart(): ShopifyBuy.Cart | null { 27 | const existingCartString = get(LocalStorageKeys.CART); 28 | if (existingCartString == null) { 29 | return null; 30 | } 31 | 32 | try { 33 | const existingCart = JSON.parse(existingCartString); 34 | if (!isCart(existingCart)) { 35 | return null; 36 | } 37 | 38 | return existingCart as ShopifyBuy.Cart; 39 | } catch { 40 | return null; 41 | } 42 | } 43 | 44 | export const LocalStorage = { 45 | get, 46 | set, 47 | getInitialCart, 48 | }; 49 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/LocalStorage/__tests__/LocalStorage.test.ts: -------------------------------------------------------------------------------- 1 | import {LocalStorage} from '../../LocalStorage'; 2 | import {Mocks} from '../../../mocks'; 3 | import {LocalStorageKeys} from '../keys'; 4 | 5 | describe('LocalStorage.set()', () => { 6 | it('sets a key in localStorage', () => { 7 | const key = 'checkoutId'; 8 | const value = 'checkout_1'; 9 | 10 | const setItemSpy = jest.spyOn(window.localStorage, 'setItem'); 11 | LocalStorage.set(key, value); 12 | 13 | expect(setItemSpy).toHaveBeenCalledWith(key, value); 14 | }); 15 | }); 16 | 17 | describe('LocalStorage.get()', () => { 18 | it('gets a value from localStorage', () => { 19 | const key = 'checkoutId'; 20 | const value = 'checkout_1'; 21 | LocalStorage.set(key, value); 22 | 23 | const getItemSpy = jest.spyOn(window.localStorage, 'getItem'); 24 | const newValue = LocalStorage.get(key); 25 | 26 | expect(newValue).toBe(value); 27 | expect(getItemSpy).toHaveBeenCalledWith(key); 28 | }); 29 | 30 | it('returns null when there is no value in localStorage', () => { 31 | const key = 'unknown_key'; 32 | 33 | const getItemSpy = jest.spyOn(window.localStorage, 'getItem'); 34 | const newValue = LocalStorage.get(key); 35 | 36 | expect(newValue).toBeNull(); 37 | expect(getItemSpy).toHaveBeenCalledWith(key); 38 | }); 39 | }); 40 | 41 | describe('LocalStorage.getInitialCart()', () => { 42 | it('returns a cart object if it exists', () => { 43 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(Mocks.CART)); 44 | expect(LocalStorage.getInitialCart()).toEqual(Mocks.CART); 45 | }); 46 | 47 | it('returns null if there is no stored object', () => { 48 | LocalStorage.set(LocalStorageKeys.CART, ''); 49 | expect(LocalStorage.getInitialCart()).toBeNull(); 50 | }); 51 | 52 | it('returns null if the stored object is invalid JSON', () => { 53 | LocalStorage.set(LocalStorageKeys.CART, "{id: 'asdf', lineItems: []"); 54 | expect(LocalStorage.getInitialCart()).toBeNull(); 55 | }); 56 | 57 | it('returns null if the stored object is not a valid cart', () => { 58 | const badCart = {...Mocks.CART, type: {}}; 59 | LocalStorage.set(LocalStorageKeys.CART, JSON.stringify(badCart)); 60 | 61 | expect(LocalStorage.getInitialCart()).toBeNull(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/LocalStorage/index.ts: -------------------------------------------------------------------------------- 1 | import {LocalStorage} from './LocalStorage'; 2 | import {LocalStorageKeys} from './keys'; 3 | 4 | export {LocalStorage, LocalStorageKeys}; 5 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/LocalStorage/keys.ts: -------------------------------------------------------------------------------- 1 | const CART = 'shopify_local_store__cart'; 2 | const CHECKOUT_ID = 'shopify_local_store__checkout_id'; 3 | 4 | export const LocalStorageKeys = { 5 | CART, 6 | CHECKOUT_ID, 7 | }; 8 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import {useCoreOptions} from './useCoreOptions'; 2 | import {LocalStorage, LocalStorageKeys} from './LocalStorage'; 3 | import {isCart} from './types'; 4 | 5 | export {useCoreOptions, LocalStorage, LocalStorageKeys, isCart}; 6 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/types/__tests__/isCart.test.ts: -------------------------------------------------------------------------------- 1 | import {isCart} from '../isCart'; 2 | import {Mocks} from '../../../mocks'; 3 | 4 | describe('isCart()', () => { 5 | it('returns true for an input that is a valid Cart', () => { 6 | expect(isCart(Mocks.CART)).toBe(true); 7 | }); 8 | 9 | it('returns false for an input that is not a valid Cart', () => { 10 | const badCart = { 11 | id: 'some id', 12 | lineItems: null, 13 | }; 14 | 15 | expect(isCart(badCart)).toBe(false); 16 | expect(isCart('')).toBe(false); 17 | expect(isCart({})).toBe(false); 18 | expect(isCart(null)).toBe(false); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/types/index.ts: -------------------------------------------------------------------------------- 1 | export {isCart} from './isCart'; 2 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/types/isCart.ts: -------------------------------------------------------------------------------- 1 | import ShopifyBuy from 'shopify-buy'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | export function isCart(potentialCart: any): potentialCart is ShopifyBuy.Cart { 5 | return ( 6 | potentialCart != null && 7 | potentialCart.id != null && 8 | potentialCart.webUrl != null && 9 | potentialCart.lineItems != null && 10 | potentialCart.type != null && 11 | potentialCart.type.name === 'Checkout' && 12 | potentialCart.type.kind === 'OBJECT' 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/useCoreOptions/__tests__/useCoreOptions.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from '@testing-library/react'; 3 | import {useCoreOptions} from '../useCoreOptions'; 4 | import {CoreOptions} from '../types'; 5 | import {useStaticQuery} from 'gatsby'; 6 | 7 | const mockAccessToken = 'THIS_IS_MY_ACCESS_TOKEN'; 8 | const mockShopName = 'fake-shop'; 9 | 10 | beforeEach(() => { 11 | (useStaticQuery as jest.Mock<{ 12 | coreOptions: CoreOptions; 13 | }>).mockImplementationOnce(() => ({ 14 | coreOptions: { 15 | accessToken: mockAccessToken, 16 | shopName: mockShopName, 17 | }, 18 | })); 19 | }); 20 | 21 | describe('useCoreOptions()', () => { 22 | it('returns a shopName', () => { 23 | function MockComponent() { 24 | const {shopName} = useCoreOptions(); 25 | return

{shopName}

; 26 | } 27 | 28 | const wrapper = render(); 29 | 30 | expect(wrapper.getByText(mockShopName)).toHaveTextContent(mockShopName); 31 | }); 32 | 33 | it('returns an accessToken', () => { 34 | function MockComponent() { 35 | const {accessToken} = useCoreOptions(); 36 | return

{accessToken}

; 37 | } 38 | 39 | const wrapper = render(); 40 | 41 | expect(wrapper.getByText(mockAccessToken)).toHaveTextContent( 42 | mockAccessToken, 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/useCoreOptions/index.ts: -------------------------------------------------------------------------------- 1 | export {useCoreOptions} from './useCoreOptions'; 2 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/useCoreOptions/types.ts: -------------------------------------------------------------------------------- 1 | export interface CoreOptions { 2 | shopName: string; 3 | accessToken: string; 4 | } 5 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/src/utils/useCoreOptions/useCoreOptions.ts: -------------------------------------------------------------------------------- 1 | import {useStaticQuery, graphql} from 'gatsby'; 2 | import {CoreOptions} from './types'; 3 | 4 | interface CoreOptionsQueryShape { 5 | coreOptions: CoreOptions; 6 | } 7 | 8 | export function useCoreOptions() { 9 | const {coreOptions}: CoreOptionsQueryShape = useStaticQuery(graphql` 10 | query CoreOptionsQuery { 11 | coreOptions(id: {eq: "gatsby-theme-shopify-manager"}) { 12 | shopName 13 | accessToken 14 | } 15 | } 16 | `); 17 | 18 | return coreOptions; 19 | } 20 | -------------------------------------------------------------------------------- /gatsby-theme-shopify-manager/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | /* Strict Type-Checking Options */ 23 | "strict": true /* Enable all strict type-checking options. */, 24 | "skipLibCheck": true, 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | /* Module Resolution Options */ 37 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | /* Source Map Options */ 47 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | /* Experimental Options */ 52 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 53 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(tsx?|jsx?)$': `/config/jest-preprocess.js`, 4 | }, 5 | moduleNameMapper: { 6 | '.+\\.(css|styl|less|sass|scss)$': `identity-obj-proxy`, 7 | '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `/config/__mocks__/file-mocks.js`, 8 | }, 9 | testPathIgnorePatterns: [`node_modules`, `.cache`, `public`], 10 | transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`], 11 | globals: { 12 | __PATH_PREFIX__: ``, 13 | }, 14 | testURL: `http://localhost`, 15 | setupFiles: [ 16 | `/config/loadershim.js`, 17 | `/config/__mocks__/browser-mocks.js`, 18 | ], 19 | setupFilesAfterEnv: ['/config/setup-test-env.js'], 20 | testRegex: '(/__tests__/.*|\\.(test|spec))\\.(jsx?|tsx?)$', 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "yarn workspace docs develop", 5 | "start-broadcast": "yarn workspace docs develop-broadcast", 6 | "test": "jest --watch", 7 | "test-build": "jest", 8 | "lint": "eslint . --ext ts --ext tsx --ext js --ext jsx", 9 | "lint:fix": "yarn lint --fix", 10 | "pretty": "prettier --write \"**/*.{ts,tsx,js,css,scss}\"", 11 | "type-check": "yarn workspace gatsby-theme-shopify-manager tsc --noEmit", 12 | "build-docs": "yarn workspace docs build", 13 | "check-all": "yarn lint && yarn type-check && yarn test-build" 14 | }, 15 | "workspaces": [ 16 | "gatsby-theme-shopify-manager", 17 | "docs" 18 | ], 19 | "devDependencies": { 20 | "@testing-library/jest-dom": "^5.11.4", 21 | "@testing-library/react": "^11.0.2", 22 | "@testing-library/react-hooks": "^3.4.1", 23 | "@types/jest": "^25.1.3", 24 | "@typescript-eslint/eslint-plugin": "^2.20.0", 25 | "@typescript-eslint/parser": "^2.20.0", 26 | "babel-jest": "^25.1.0", 27 | "babel-preset-gatsby": "^0.2.29", 28 | "eslint": "^6.8.0", 29 | "eslint-config-prettier": "^6.10.0", 30 | "eslint-plugin-jest": "^23.7.0", 31 | "eslint-plugin-prettier": "^3.1.2", 32 | "eslint-plugin-react": "^7.18.3", 33 | "identity-obj-proxy": "^3.0.0", 34 | "jest": "^25.1.0", 35 | "prettier": "^1.19.1", 36 | "react-test-renderer": "^16.13.0" 37 | }, 38 | "version": "0.0.0" 39 | } 40 | --------------------------------------------------------------------------------