├── .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 |
45 | Add Item
46 |
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 |
72 | Remove Item
73 |
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 |
97 | Empty Cart
98 |
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 |
92 |
93 | Illustrations are from {openPeepsLink} by {pabloStanleyLink}. Built
94 | with {heartEmoji} by {twitterLink}.
95 |
96 |
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 |
27 | Add an item to your cart
28 |
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 |
31 | Add items to your cart
32 |
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 |
16 | {cartItems.map((lineItem) => (
17 |
18 | {lineItem.title} - {lineItem.variant.title}
19 |
20 | ))}
21 |
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 |
32 | {cartItems.map((lineItem) => (
33 |
34 | {lineItem.title} - {lineItem.variant.title}
35 |
36 | ))}
37 |
38 | >
39 | ) : (
40 | Your cart is empty.
41 | );
42 |
43 | return (
44 | <>
45 | {cartMarkup}
46 |
47 | Remove item from your cart
48 |
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 |
32 | {cartItems.map((lineItem) => (
33 |
34 | {lineItem.title} - {lineItem.variant.title}
35 |
36 | ))}
37 |
38 | >
39 | ) : (
40 | Your cart is empty.
41 | );
42 |
43 | return (
44 | <>
45 | {cartMarkup}
46 |
47 | Remove items from your cart
48 |
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 | Update quantity
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 |
54 | {hooksDocumentationFiles.map((hook) => {
55 | const hookLink = hook.name.replace(/[^a-z]/gi, '').toLowerCase();
56 |
57 | return (
58 |
59 | {hook.name}
60 |
61 | );
62 | })}
63 |
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 |
38 | Add an item to your cart
39 |
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 |
42 | Add items to your cart
43 |
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 |
28 | {cartItems.map((lineItem) => (
29 |
30 | {lineItem.title} - {lineItem.variant.title}
31 |
32 | ))}
33 |
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 |
44 | {cartItems.map((lineItem) => (
45 |
46 | {lineItem.title} - {lineItem.variant.title}
47 |
48 | ))}
49 |
50 | >
51 | ) : (
52 | Your cart is empty.
53 | );
54 |
55 | return (
56 | <>
57 | {cartMarkup}
58 |
59 | Remove item from your cart
60 |
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 |
44 | {cartItems.map((lineItem) => (
45 |
46 | {lineItem.title} - {lineItem.variant.title}
47 |
48 | ))}
49 |
50 | >
51 | ) : (
52 | Your cart is empty.
53 | );
54 |
55 | return (
56 | <>
57 | {cartMarkup}
58 |
59 | Remove items from your cart
60 |
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 | Update quantity
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 | 
2 |
3 | [](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 |
--------------------------------------------------------------------------------