├── .circleci
└── config.yml
├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── npm-publish.yml
│ └── pull-request.yml
├── .gitignore
├── .nowignore
├── .prettierrc
├── LICENSE
├── README.md
├── babel.config.js
├── example
└── index.html
├── now.json
├── package.json
├── renovate.json
├── src
├── components
│ ├── AddressFields.js
│ ├── AddressPreview.js
│ ├── Button.js
│ ├── BuyButton.js
│ ├── Cart.js
│ ├── CartButton.js
│ ├── CartItem.js
│ ├── CartItemList.js
│ ├── Checkbox.js
│ ├── Checkout.js
│ ├── Error.js
│ ├── Grid.js
│ ├── Input.js
│ ├── Label.js
│ ├── LoginButton.js
│ ├── LoginForm.js
│ ├── Modal
│ │ ├── Footer.js
│ │ ├── Header.js
│ │ └── index.js
│ ├── OrderConfirmation.js
│ ├── OrderItem.js
│ ├── OrderList.js
│ ├── PlacesSuggest.js
│ ├── PoweredBy.js
│ ├── PromotionManager
│ │ ├── Form.js
│ │ ├── Promotion.js
│ │ └── index.js
│ ├── QuantityStepper.js
│ ├── Select.js
│ ├── ShopkitIcon.js
│ └── typography.js
├── countries.js
├── hooks
│ ├── useLocalStorage.js
│ ├── useOnClickOutside.js
│ └── useScript.js
├── index.js
├── model
│ ├── cart.js
│ ├── checkout.js
│ ├── index.js
│ ├── modal.js
│ └── user.js
├── theme.js
├── utils.js
└── validation
│ ├── auth.js
│ └── checkout.js
├── webpack.config.js
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: 'circleci/node:latest'
6 | steps:
7 | - checkout
8 | - run: yarn
9 | - run: yarn build
10 | - save_cache:
11 | paths:
12 | - node_modules
13 | key: moltin-shopkit-btn-{{ checksum "package.json" }}
14 | release:
15 | docker:
16 | - image: 'circleci/node:latest'
17 | steps:
18 | - checkout
19 | - restore_cache:
20 | keys:
21 | - moltin-shopkit-btn-{{ checksum "package.json" }}
22 | - moltin-shopkit-btn-
23 | - run: npx semantic-release
24 | workflows:
25 | version: 2
26 | build_test_release:
27 | jobs:
28 | - build
29 | - release:
30 | filters:
31 | branches:
32 | only: master
33 | requires:
34 | - build
35 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Before contributing to this repository, discuss the changes through an issue or any appropriate mode of communication.
4 |
5 | Follow the code of conduct in all interactions with the project.
6 |
7 | ### Reporting Bugs
8 |
9 | Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).
10 |
11 | 1. Create an issue and provide the required information in the [issue template](ISSUE_TEMPLATE.md).
12 | 2. Describe the problem with the following details:
13 | - A self-explanatory title.
14 | - Steps to reproduce the problem. Provide the steps with details such as, what are the steps and how to perform the steps.
15 | - Examples to demonstrate the steps. Include links to files or reusable snippets that are used in the examples. For providing snippets in an issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
16 | - Describe the result that you get when you follow the steps and state the issues, if any.
17 | - Explain the expected result and the reason.
18 | - Add screenshots for the results of each step.
19 | 3. Add the following configuration and environment details:
20 | - Versions of node and npm that you use. Run the `node --version` and `npm --version` command in the terminal to get the correct version.
21 | - Name and version of the operating system that you use.
22 | - Packages installed on your machine. Review your `package.json` file to get the list. Ensure that new packages are installed successfully during the development.
23 | - The keyboard layout. For example, US layout.
24 |
25 | ### Suggesting Enhancements
26 |
27 | This section provides guidelines to submit enhancement suggestions, such as new features or improvements to existing features.
28 |
29 | - When you create an enhancement suggestion, populate the [issue template](ISSUE_TEMPLATE.md) and provide the steps to implement the enhancement.
30 | - Review the [How Do I Submit A (Good) Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) section.
31 |
32 | #### How Do I Submit Enhancement Suggestions?
33 |
34 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/).
35 |
36 | 1. Create an issue and provide the following information:
37 | - A self-explanatory title.
38 | - Detailed instructions to implement the enhancement and to understand the enhancement suggestion.
39 | - Examples to demonstrate the steps. Include links to files or reusable snippets that are used in the examples. For providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
40 | - Describe the current functionality and the expected functionality with enhancement and provide the reason.
41 | 2. Explain the benefits of the enhancement.
42 |
43 | ### Pull Request Process
44 |
45 | You can contribute to this project by creating a pull request in the repository.
46 |
47 | 1. Fork the repository into your Github account.
48 | 2. Clone the fork locally.
49 | 3. Create a git branch.
50 | 4. Set up the development environment and install the required dependencies. For more information, see [Setup](https://github.com/moltin/shopkit/blob/master/README.md).
51 | 5. Make the changes and commit the changes to your new branch. Ensure that the code is clear and comprehensible.
52 | 6. Create a Pull Request (PR).
53 | 7. Incorporate the review comments provided by the repository owner, if any, and get approval.
54 | The repository owner merges all approved PRs to the repository.
55 |
56 | ### Elastic Path Code of Conduct
57 |
58 | Elastic Path suggests the following code of conduct in all communications. For example:
59 |
60 | * Use welcoming and inclusive language
61 | * Respect others viewpoints and experiences
62 | * Accept constructive criticism
63 |
64 | The contributors must follow the code of conduct in the project and public spaces
65 | when they represent the project or the community. For example, when you use an official project e-mail address, official social media account, or represent the community at an online or offline event.
66 |
67 | ### Rights of the Repository Owners
68 |
69 | - Define, implement, and assure code of conduct and take action if anyone breaches the standards.
70 | - Remove, edit, or reject comments, commits, code, wiki edits, issues, or other contributions that do not align with the code of conduct.
71 | - Restrict the access of contributors temporarily or permanently when the code of conduct is violated.
72 | - Define and clarify the representation of a project further.
73 |
74 |
75 | ### Attribution
76 |
77 | Elastic Path code of conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org/version/1/4) 1.4.
78 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Step 1: Are you in the right place?
2 |
3 | * For issues or feature requests related to the code **in this repository** file a Github issue.
4 | * For issues relating to EP in relation to the store please Open a Support ticket.
5 |
6 | ### Step 2: Describe your environment
7 |
8 | ```
9 | Issues submitted without sufficient information will be rejected.
10 | ```
11 |
12 | * Device: _____
13 | * OS version: _____
14 | * @moltin/request version: _____
15 | * Node version: _____
16 |
17 | ### Step 3: Describe the problem:
18 |
19 | #### Steps to reproduce:
20 |
21 | 1. _____
22 | 2. _____
23 | 3. _____
24 |
25 | #### Observed Results:
26 |
27 | * What happened? This could be a description, console log output, etc.
28 |
29 | #### Expected Results:
30 |
31 | * What did you expect to happen?
32 |
33 | #### Relevant Code:
34 |
35 | ```
36 | // TODO(you): code here to reproduce the problem
37 | ```
38 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Type
2 |
3 | * ### Feature
4 |
5 | * ### Fix
6 |
7 | * ### Docs
8 |
9 | * ### Style
10 |
11 | * ### Refactor
12 |
13 | * ### Performance
14 |
15 | * ### Test
16 |
17 | * ### Chore
18 |
19 |
20 | ## Description
21 |
22 |
23 |
24 | ## Issues
25 |
26 |
27 |
28 | * Fixes [MT-]
29 |
30 | ## Dependencies
31 |
32 |
33 |
34 | ## Notes
35 |
36 |
37 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish NPM
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v1
13 | - name: Yarn CLI
14 | uses: CultureHQ/actions-yarn@v1.0.1
15 | - name: Install dependencies
16 | run: yarn install
17 | - name: Build app
18 | run: yarn build
19 |
20 | release:
21 | name: Release
22 | needs: build
23 | runs-on: ubuntu-18.04
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v1
27 | - name: Setup Node.js
28 | uses: actions/setup-node@v1
29 | with:
30 | node-version: 12
31 | - name: Yarn CLI
32 | uses: CultureHQ/actions-yarn@v1.0.1
33 | - name: Install dependencies
34 | run: yarn install
35 | - name: Build app
36 | run: yarn prod-build
37 | - name: Remove node_modules
38 | run: rm -rf node_modules
39 | - name: Release
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 | run: npx semantic-release --no-ci
44 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yml:
--------------------------------------------------------------------------------
1 | name: Check PR
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | check-pr-title:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: aslafy-z/conventional-pr-title-action@master
13 | with:
14 | success-state: Title follows the specification.
15 | failure-state: Title does not follow the specification.
16 | context-name: conventional-pr-title
17 | preset: conventional-changelog-angular@latest
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 |
21 | build:
22 | runs-on: ubuntu-latest
23 | needs: check-pr-title
24 | steps:
25 | - uses: actions/checkout@v1
26 | - name: Yarn CLI
27 | uses: CultureHQ/actions-yarn@v1.0.1
28 | - name: Install dependencies
29 | run: yarn install
30 | - name: Build app
31 | run: yarn build
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | dist
4 |
5 | .env
6 | .DS_Store
7 | *.log
--------------------------------------------------------------------------------
/.nowignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | example
4 | README.md
5 | .DS_Store
6 | *.log
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "none"
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 shopkit
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 |
2 |
3 | # Elastic Path Commerce Cloud Embeddable Cart + Checkout
4 |
5 | [](https://github.com/moltin/shopkit)
6 | [](https://opensource.org/licenses/MIT)
7 | [](https://github.com/moltin/shopkit/issues)
8 | [](https://twitter.com/intent/follow?screen_name=elasticpath)
9 |
10 | > A buy button, a cart, a checkout, a customer login. All without writing a single line of JavaScript!
11 |
12 | [Demo](https://embedded-commerce.elasticpath.com/)
13 |
14 | ## Documentation
15 |
16 | - [Shopkit Documentation](https://documentation.elasticpath.com/commerce-cloud/docs/developer/get-started/shopkit-demo.html)
17 | - [Shopkit Embeddable Cart + Checkout](https://www.elasticpath.com/product/application-library/embedded-commerce-details)
18 |
19 | ## Contributing
20 |
21 | 1. Clone repo
22 | 2. Use `yarn` to install dependencies
23 | 3. Run `yarn dev` and webpack will open `example/index.html`
24 |
25 | The example playground uses the `demo.elasticpath.com` API keys.
26 |
27 | ## Publishing
28 |
29 | Shopkit versioning is based on semver and the angular commit convention. Releasing is automatically triggered by semantic-release.
30 |
31 | We should recommend users use the specific version `unpkg` url, e.g. `https://unpkg.com/@moltin/shopkit@1.0.0/index.js`.
32 |
33 | You can optionally deploy to `btn.moltin.com` which should be used for those wanting to develop on master. You can deploy by running `yarn deploy`.
34 |
35 | ## Terms And Conditions
36 |
37 | - Any changes to this project must be reviewed and approved by the repository owner. For more information about contributing, see the [Contribution Guide](https://github.com/moltin/shopkit/blob/master/.github/CONTRIBUTING.md).
38 | - For more information about the license, see [MIT License](https://github.com/moltin/shopkit/blob/master/LICENSE).
39 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/env',
5 | {
6 | targets: {
7 | node: 'current'
8 | }
9 | }
10 | ],
11 | '@babel/react'
12 | ],
13 | plugins: [
14 | '@babel/plugin-proposal-class-properties',
15 | [
16 | 'babel-plugin-styled-components',
17 | {
18 | ssr: false
19 | }
20 | ]
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Moltin Shopkit
8 |
16 |
17 |
18 |
19 |
20 |
25 |
29 |
30 |
35 |
40 |
48 |
49 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "btn",
4 | "scope": "moltin",
5 | "alias": "btn.moltin.com",
6 | "regions": ["all"],
7 | "builds": [{ "src": "package.json", "use": "@now/static-build" }]
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@moltin/shopkit",
3 | "version": "0.0.0-development",
4 | "description": "Embeddable Moltin Cart + Checkout",
5 | "main": "dist/index.js",
6 | "repository": "moltin/shopkit",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "dev": "webpack-dev-server",
12 | "deploy": "now --target=production",
13 | "build": "webpack",
14 | "prod-build": "webpack --mode=production",
15 | "now-build": "webpack --mode=production",
16 | "semantic-release": "semantic-release"
17 | },
18 | "keywords": [
19 | "moltin",
20 | "buy button",
21 | "embeddable cart"
22 | ],
23 | "author": "Jamie Barton ",
24 | "license": "MIT",
25 | "dependencies": {
26 | "@moltin/request": "2.0.2",
27 | "algolia-places-react": "1.6.1",
28 | "arrive": "2.4.1",
29 | "easy-peasy": "2.6.6",
30 | "final-form": "4.20.4",
31 | "react": "16.14.0",
32 | "react-dom": "16.14.0",
33 | "react-final-form": "4.1.0",
34 | "react-stripe-elements": "2.0.3",
35 | "styled-components": "4.4.1"
36 | },
37 | "devDependencies": {
38 | "@babel/core": "7.16.0",
39 | "@babel/plugin-proposal-class-properties": "7.16.0",
40 | "@babel/preset-env": "7.16.4",
41 | "@babel/preset-react": "7.16.0",
42 | "babel-loader": "8.2.3",
43 | "babel-plugin-styled-components": "1.13.3",
44 | "css-loader": "2.1.1",
45 | "mini-css-extract-plugin": "0.12.0",
46 | "optimize-css-assets-webpack-plugin": "5.0.8",
47 | "prop-types": "15.7.2",
48 | "terser-webpack-plugin": "1.4.5",
49 | "webpack": "4.46.0",
50 | "webpack-cli": "3.3.12",
51 | "webpack-dev-server": "3.11.3",
52 | "webpack-obfuscator": "0.28.5",
53 | "semantic-release": "17.4.7"
54 | },
55 | "release": {
56 | "branches": ["master", "next"]
57 | },
58 | "publishConfig": {
59 | "access": "public"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base", "docker:disable", "schedule:weekly"],
3 | "automerge": true,
4 | "major": {
5 | "automerge": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/AddressFields.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import styled from 'styled-components'
3 |
4 | import { TextButton } from './Button'
5 | import Input from './Input'
6 | import Select from './Select'
7 | import PlacesSuggest from './PlacesSuggest'
8 | import { Grid, GridCol } from './Grid'
9 |
10 | import countryOptions from '../countries'
11 |
12 | const SearchWrapper = styled.div`
13 | padding: 0 0 1.5rem;
14 | `
15 |
16 | const FieldsWrapper = styled.div`
17 | border-top: 1px solid ${props => props.theme.divider};
18 | padding: 0.75rem 0 1.5rem;
19 | `
20 |
21 | const StyledTextButton = styled(TextButton)`
22 | margin-top: 0.5rem !important;
23 | display: inline-block !important;
24 | `
25 |
26 | export default function AddressFields({
27 | isEditing = false,
28 | type: shortType,
29 | form
30 | }) {
31 | const [editing, setEditing] = useState(isEditing)
32 | const type = `${shortType}_address`
33 | const isShipping = shortType === 'shipping'
34 |
35 | function onPlacesChange(type, { name, city, county, countryCode, postcode }) {
36 | form.change(`${type}.line_1`, name)
37 | form.change(`${type}.city`, city)
38 | form.change(`${type}.county`, county)
39 | form.change(`${type}.country`, countryCode.toUpperCase())
40 | form.change(`${type}.postcode`, postcode)
41 | }
42 |
43 | function onPlacesClear(type) {
44 | form.change(`${type}.line_1`, '')
45 | form.change(`${type}.city`, '')
46 | form.change(`${type}.county`, '')
47 | form.change(`${type}.country`, '')
48 | form.change(`${type}.postcode`, '')
49 | }
50 |
51 | return (
52 |
53 |
54 | {
57 | onPlacesChange(type, suggestion)
58 | setEditing(true)
59 | }}
60 | onClear={() => onPlacesClear(type)}
61 | />
62 |
63 | {!editing && (
64 | setEditing(true)}>
65 | Enter address manually
66 |
67 | )}
68 |
69 |
70 | {editing && (
71 |
72 |
73 |
74 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
107 |
108 |
109 |
110 |
116 |
117 |
118 |
119 | {isShipping && (
120 |
121 |
122 |
123 |
128 |
129 | )}
130 |
131 | )}
132 |
133 | )
134 | }
135 |
--------------------------------------------------------------------------------
/src/components/AddressPreview.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import { TextButton } from './Button'
5 |
6 | const Wrapper = styled.div`
7 | border-radius: 0.25rem;
8 | border: 1px solid ${props => props.theme.border};
9 | padding: 1rem;
10 | margin: 1.5rem 0;
11 | `
12 |
13 | const Heading = styled.div`
14 | align-items: center;
15 | display: flex;
16 | justify-content: space-between;
17 | margin-bottom: 1rem;
18 | `
19 |
20 | const Title = styled.p`
21 | margin: 0;
22 | font-weight: 500;
23 | `
24 |
25 | const Content = styled.p`
26 | margin: 0;
27 | `
28 |
29 | function AddressPreview({ type: shortType, handleClick, address }) {
30 | const type = `${shortType} address`
31 | const formattedAddress = address
32 | ? Object.values({
33 | line_1: address.line_1,
34 | ...(address.line_2 && { line_2: address.line_2 }),
35 | city: address.city,
36 | county: address.county,
37 | postcode: address.postcode
38 | })
39 | .join(', ')
40 | .slice(0, -1)
41 | : null
42 |
43 | if (!formattedAddress) return null
44 |
45 | return (
46 |
47 |
48 | {type}
49 | Change
50 |
51 | {formattedAddress}
52 |
53 | )
54 | }
55 |
56 | export default AddressPreview
57 |
--------------------------------------------------------------------------------
/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { css } from 'styled-components'
3 |
4 | const SVG = styled.svg`
5 | display: inline;
6 | margin-right: 0.25rem;
7 | `
8 |
9 | const Spinner = () => (
10 |
19 |
23 |
32 |
33 |
34 | )
35 |
36 | const StyledButton = styled.button.attrs({
37 | className: 'moltin-shopkit shopkit-button'
38 | })`
39 | background: none;
40 | box-sizing: border-box;
41 | line-height: 1.15;
42 | -webkit-text-size-adjust: 100%;
43 | margin: 0;
44 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
45 | Helvetica Neue, Arial, Noto Sans, sans-serif;
46 | font-size: 15px;
47 | appearance: none;
48 | border-radius: 0.25rem;
49 | border: 1px solid transparent;
50 | color: ${props => props.theme.placeholder};
51 | font-weight: 500;
52 | padding: ${props => (props.noPadding ? 0 : '0 1rem')};
53 | outline: none;
54 | opacity: ${props => (props.disabled ? 0.5 : 1)};
55 |
56 | &::before,
57 | &::after {
58 | box-sizing: inherit;
59 | -webkit-font-smoothing: antialiased;
60 | }
61 |
62 | &:hover,
63 | &:focus {
64 | outline: none;
65 | }
66 |
67 | &::-moz-focus-inner {
68 | border-style: none;
69 | padding: 0;
70 | }
71 |
72 | &:hover {
73 | cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
74 | }
75 |
76 | ${({ block }) =>
77 | block &&
78 | css`
79 | width: 100%;
80 | `};
81 |
82 | ${({ marginTop }) =>
83 | marginTop &&
84 | css`
85 | margin-top: 1.5rem;
86 | `};
87 | `
88 |
89 | function Button({ loading, children, ...rest }) {
90 | return (
91 | {loading ? : children}
92 | )
93 | }
94 |
95 | export const PrimaryButton = styled(Button).attrs({
96 | className: 'shopkit-primary-button shopkit-primary'
97 | })`
98 | background-color: ${props => props.theme.primary};
99 | border-color: ${props => props.theme.white};
100 | color: ${props => props.theme.white};
101 | height: 2.8rem;
102 | display: inline-flex;
103 | align-items: center;
104 | justify-content: center;
105 |
106 | ${({ large }) =>
107 | large &&
108 | css`
109 | height: 3.25rem;
110 | `};
111 | `
112 |
113 | export const TextButton = styled(Button)`
114 | color: ${props => props.theme.dark};
115 | font-weight: 500;
116 | font-size: ${props => props.theme.textSmall} !important;
117 | text-decoration: underline;
118 | padding: 0;
119 |
120 | &:hover {
121 | color: ${props => props.theme.primary};
122 | }
123 | `
124 |
125 | export default Button
126 |
--------------------------------------------------------------------------------
/src/components/BuyButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useActions } from 'easy-peasy'
3 |
4 | import { PrimaryButton } from './Button'
5 |
6 | function BuyButton({
7 | moltinProductId,
8 | moltinOpenCart,
9 | moltinType,
10 | moltinText,
11 | ...props
12 | }) {
13 | if (moltinType !== 'custom' && !moltinProductId) {
14 | console.warn('No product ID provided to Moltin Btn.')
15 | return null
16 | }
17 |
18 | const { addToCart } = useActions(({ cart }) => cart)
19 | const { goToCart } = useActions(({ modal }) => modal)
20 |
21 | function add() {
22 | moltinType !== 'custom'
23 | ? addToCart({ id: moltinProductId })
24 | : addToCart({
25 | type: 'custom_item',
26 | name: props.moltinProductName,
27 | sku: props.moltinProductSku,
28 | price: {
29 | amount: parseInt(props.moltinProductPrice, 10)
30 | }
31 | })
32 |
33 | moltinOpenCart && goToCart()
34 | }
35 |
36 | return (
37 |
38 | {moltinText}
39 |
40 | )
41 | }
42 |
43 | BuyButton.defaultProps = {
44 | moltinText: 'Add to Cart',
45 | moltinOpenCart: false
46 | }
47 |
48 | export default BuyButton
49 |
--------------------------------------------------------------------------------
/src/components/Cart.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useStore, useActions } from 'easy-peasy'
3 | import styled from 'styled-components'
4 |
5 | import { PrimaryButton } from './Button'
6 | import { Heading } from './typography'
7 | import CartItemList from './CartItemList'
8 | import { RouteHeader } from './Modal/Header'
9 | import { pluralize } from '../utils'
10 |
11 | export default function Cart() {
12 | const { isEmpty, cartItems, promotionItems, subTotal, count } = useStore(
13 | ({ cart }) => cart
14 | )
15 | const { goToShipping } = useActions(({ modal }) => modal)
16 |
17 | return (
18 |
19 |
20 | Your shopping cart
21 |
22 |
23 | {isEmpty ? (
24 | Your cart is empty
25 | ) : (
26 |
27 |
32 |
33 | {!isEmpty && (
34 |
35 |
36 | Total
37 | {subTotal}
38 |
39 |
40 |
41 | Checkout with {pluralize(count, 'item')}
42 |
43 |
44 | )}
45 |
46 | )}
47 |
48 | )
49 | }
50 |
51 | const StyledCart = styled.div.attrs({
52 | className: 'moltin-shopkit shopkit-cart'
53 | })``
54 |
55 | const CartEmpty = styled.p`
56 | color: ${props => props.theme.dark};
57 | text-align: center;
58 | margin: 1.5rem 0;
59 | `
60 |
61 | const CartTotalRow = styled.div`
62 | border-top: 1px solid ${props => props.theme.divider};
63 | padding: 1.5rem 0;
64 | display: flex;
65 | align-items: center;
66 | justify-content: space-between;
67 | `
68 |
69 | const CartTotalTitle = styled.span`
70 | color: ${props => props.theme.dark};
71 | font-weight: 500;
72 | font-size: ${props => props.theme.textLarge};
73 | `
74 |
75 | const CartTotalSubTotal = styled.span`
76 | color: ${props => props.theme.dark};
77 | font-weight: 500;
78 | font-size: ${props => props.theme.textExtraLarge};
79 | `
80 |
--------------------------------------------------------------------------------
/src/components/CartButton.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useStore, useActions } from 'easy-peasy'
3 | import { createCartIdentifier } from '@moltin/request'
4 |
5 | import { PrimaryButton } from './Button'
6 | import { pluralize } from '../utils'
7 | import useLocalStorage from '../hooks/useLocalStorage'
8 |
9 | function CartButton({ moltinText, moltinShowTotal }) {
10 | const { count, subTotal } = useStore(({ cart }) => cart)
11 | const {
12 | initialize,
13 | modal: { goToCart }
14 | } = useActions(actions => actions)
15 |
16 | const [cartId, setCartId] = useLocalStorage('mcart', createCartIdentifier())
17 | const [customerToken, setCustomerToken] = useLocalStorage('mtoken', null)
18 | const [customerId, setCustomerId] = useLocalStorage('mcustomer', null)
19 |
20 | const btnSuffix =
21 | subTotal || count
22 | ? ` (${moltinShowTotal ? subTotal : pluralize(count, 'item')})`
23 | : null
24 |
25 | useEffect(() => {
26 | initialize({ cartId, customerToken, customerId })
27 | setCartId(cartId)
28 | }, [cartId])
29 |
30 | return (
31 |
32 | {moltinText}
33 | {btnSuffix}
34 |
35 | )
36 | }
37 |
38 | CartButton.defaultProps = {
39 | moltinText: 'Cart',
40 | moltinShowTotal: false
41 | }
42 |
43 | export default CartButton
44 |
--------------------------------------------------------------------------------
/src/components/CartItem.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import styled, { css } from 'styled-components'
3 |
4 | import { TextButton } from './Button'
5 | import ShopkitIcon from './ShopkitIcon'
6 | import QuantityStepper from './QuantityStepper'
7 |
8 | const Wrapper = styled.div`
9 | display: flex;
10 | padding: 0.75rem 0;
11 | width: 100%;
12 |
13 | ${({ removing }) =>
14 | removing &&
15 | css`
16 | opacity: 0.3;
17 | cursor: not-allowed;
18 | `};
19 | `
20 |
21 | const PhotoBox = styled.div`
22 | border: 1px solid ${props => props.theme.border};
23 | border-radius: 0.25rem;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | position: relative;
28 | margin-right: 1rem;
29 | width: 75px;
30 | height: 75px;
31 |
32 | img {
33 | max-width: 100%;
34 | border-radius: 0.25rem;
35 | overflow: hidden;
36 | }
37 | `
38 |
39 | // const Quantity = styled.span`
40 | // align-items: center;
41 | // background-color: ${props => props.theme.primary};
42 | // border-radius: 100%;
43 | // color: ${props => props.theme.white};
44 | // display: flex;
45 | // font-weight: 700;
46 | // font-size: ${props => props.theme.textSmall};
47 | // justify-content: center;
48 | // right: 0;
49 | // position: absolute;
50 | // text-align: center;
51 | // top: 0;
52 | // width: 22px;
53 | // height: 22px;
54 | // margin-top: -11px;
55 | // margin-right: -11px;
56 | // `
57 |
58 | const Info = styled.div`
59 | align-items: center;
60 | display: flex;
61 | flex: 1;
62 | font-size: ${props => props.theme.textBase};
63 | justify-content: space-between;
64 | color: ${props => props.theme.dark};
65 | margin: 0;
66 | line-height: 1.5;
67 | `
68 |
69 | const ProductName = styled.p`
70 | color: ${props => props.theme.dark};
71 | font-size: ${props => props.theme.textBase};
72 | font-weight: 500;
73 | margin: 0;
74 | `
75 |
76 | const Price = styled.p`
77 | color: ${props => props.theme.placeholder};
78 | font-size: ${props => props.theme.textSmall};
79 | margin: 0;
80 | `
81 |
82 | const StyledButton = styled(TextButton)`
83 | font-size: ${props => props.theme.textSmall};
84 | text-decoration: none;
85 | margin-top: 0.5rem;
86 | `
87 |
88 | // const ProductPrice = styled.span`
89 | // color: ${props => props.theme.placeholder};
90 | // font-weight: 500;
91 | // font-size: ${props => props.theme.textSmall};
92 | // margin: 0;
93 | // `
94 |
95 | // const UnitValue = styled.span`
96 | // color: ${props => props.theme.placeholder};
97 | // margin: 0;
98 | // `
99 |
100 | const Extra = styled.div`
101 | margin-left: 1.5rem;
102 | `
103 |
104 | function CartItem({
105 | id,
106 | name,
107 | quantity,
108 | meta,
109 | image: { href },
110 | updateItem,
111 | removeFromCart
112 | }) {
113 | const {
114 | display_price: {
115 | without_tax: {
116 | unit: { formatted: unit }
117 | // value: { formatted: value }
118 | }
119 | }
120 | } = meta
121 | const [removing, setRemoving] = useState(false)
122 |
123 | const handleRemoveFromCart = async () => {
124 | await setRemoving(true)
125 | await removeFromCart(id)
126 | }
127 |
128 | const handleQuantityUpdate = async data => {
129 | const { quantity: qty } = data
130 |
131 | if (qty === 0) {
132 | handleRemoveFromCart()
133 | } else {
134 | await updateItem(data)
135 | }
136 | }
137 |
138 | return (
139 |
140 |
141 | {href ? : }
142 | {/* {quantity} */}
143 |
144 |
145 |
146 |
147 |
{name}
148 |
{unit}
149 | {/*
{value} */}
150 | {/* {quantity > 1 &&
{unit} each } */}
151 |
152 | Remove
153 |
154 |
155 |
156 |
157 |
158 |
163 |
164 |
165 | )
166 | }
167 |
168 | export default CartItem
169 |
--------------------------------------------------------------------------------
/src/components/CartItemList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useActions } from 'easy-peasy'
3 | import styled from 'styled-components'
4 |
5 | import CartItem from './CartItem'
6 | import PromotionManager from './PromotionManager'
7 |
8 | const Wrapper = styled.main`
9 | padding-bottom: 0.75rem;
10 | `
11 |
12 | export default function CartItemList({ items, promotionItems }) {
13 | const { updateItem, removeItem, addPromotion } = useActions(
14 | ({ cart }) => cart
15 | )
16 |
17 | return (
18 |
19 |
20 | {items.map(item => (
21 |
27 | ))}
28 |
29 |
30 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Field } from 'react-final-form'
3 | import styled from 'styled-components'
4 |
5 | import Label from './Label'
6 |
7 | const CheckWrapper = styled.div`
8 | align-items: center;
9 | cursor: pointer;
10 | display: flex;
11 | justify-content: center;
12 | position: relative;
13 | margin: 0 0.5rem 0 0;
14 | `
15 |
16 | const StyledCheckbox = styled.input.attrs({
17 | type: 'checkbox',
18 | className: 'shopkit-primary'
19 | })`
20 | appearance: none;
21 | background-color: ${props =>
22 | props.checked ? props.theme.primary : props.theme.white};
23 | border-radius: 0.25rem;
24 | border: 1px solid
25 | ${props => (props.checked ? props.theme.primary : props.theme.border)};
26 | outline: none;
27 | padding: 0.25rem;
28 | font-size: ${props => props.theme.textBase};
29 | cursor: pointer;
30 | width: 18px;
31 | height: 18px;
32 | margin: 0;
33 | `
34 |
35 | const CheckmarkBox = styled.span`
36 | display: flex;
37 | position: absolute;
38 | color: ${props => props.theme.white};
39 | align-items: center;
40 | justify-content: center;
41 | `
42 |
43 | const Checkmark = styled.svg`
44 | fill: currentColor;
45 | height: 12px;
46 | `
47 |
48 | export default function Checkbox({ label, name, checked, ...props }) {
49 | return (
50 |
51 | {({ input, meta }) => {
52 | const error = meta.error && meta.touched
53 | const checked = input.value
54 |
55 | return (
56 |
57 |
58 |
59 | {checked && (
60 |
61 |
68 |
69 |
73 |
74 |
79 |
80 |
81 | )}
82 |
83 | {label}
84 |
85 | )
86 | }}
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/Checkout.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useStore, useActions } from 'easy-peasy'
3 | import { Form } from 'react-final-form'
4 | import { CardElement, injectStripe } from 'react-stripe-elements'
5 | import styled from 'styled-components'
6 |
7 | import { shippingValidation, billingValidation } from '../validation/checkout'
8 | import { RouteHeader } from './Modal/Header'
9 | import { Heading } from './typography'
10 | import AddressFields from './AddressFields'
11 | import Label from './Label'
12 | import Input, { ErrorAlert } from './Input'
13 | import Checkbox from './Checkbox'
14 | import { PrimaryButton } from './Button'
15 | import AddressPreview from './AddressPreview'
16 | import OrderConfirmation from './OrderConfirmation'
17 |
18 | const Wrapper = styled.div`
19 | margin-top: 0.5rem;
20 | border-top: 1px solid ${props => props.theme.divider};
21 | padding: 0.75rem 0 1.5rem;
22 | `
23 |
24 | const StripeInput = styled.div`
25 | .StripeElement {
26 | background-color: ${props => props.theme.white};
27 | border: 1px solid ${props => props.theme.border};
28 | border-radius: 0.25rem;
29 | padding: 0.75rem 1rem;
30 | }
31 | `
32 |
33 | function Checkout({ stripe }) {
34 | const [initialValues, setInitialValues] = useState({
35 | billingIsShipping: true
36 | })
37 | const [paid, setPaid] = useState(false)
38 | const [order, setOrder] = useState(null)
39 | const { route } = useStore(({ modal }) => modal)
40 | const { id: cartId, subTotal } = useStore(({ cart }) => cart)
41 | const { id: customerId } = useStore(({ user }) => user)
42 | const { createOrder, payForOrder, setDirty } = useActions(
43 | ({ checkout }) => checkout
44 | )
45 | const { goToBilling, goToShipping } = useActions(({ modal }) => modal)
46 | const { deleteCart } = useActions(({ cart }) => cart)
47 |
48 | function validate(values) {
49 | if (route === 'shipping') {
50 | return shippingValidation(values)
51 | } else {
52 | return billingValidation(values)
53 | }
54 | }
55 |
56 | async function onSubmit(values) {
57 | if (route === 'shipping') {
58 | setInitialValues(values)
59 | goToBilling()
60 | return
61 | }
62 |
63 | let order
64 | let orderError
65 | let token
66 |
67 | const { shipping_address } = values
68 |
69 | values = {customerId, ...values}
70 | try {
71 | order = await createOrder(values)
72 | console.log({ order })
73 | setOrder(order)
74 | } catch (error) {
75 | orderError = error
76 | console.log({ orderError })
77 | }
78 |
79 | try {
80 | token = await stripe.createToken({
81 | name: `${shipping_address.first_name} ${shipping_address.last_name}`,
82 | address_line1: shipping_address.line_1,
83 | address_line2: shipping_address.line_2,
84 | address_city: shipping_address.city,
85 | address_state: shipping_address.county,
86 | address_zip: shipping_address.postcode,
87 | address_country: shipping_address.country
88 | })
89 | } catch (tokenError) {
90 | console.log('Failed to create token', tokenError)
91 | }
92 |
93 | try {
94 | const payment = await payForOrder({
95 | orderId: order.id,
96 | token: token.token.id
97 | })
98 |
99 | setPaid(true)
100 | deleteCart(cartId)
101 | } catch (paymentError) {
102 | console.log({ paymentError })
103 | }
104 | }
105 |
106 | return paid ? (
107 |
108 | ) : (
109 |
245 | )
246 | }}
247 |
248 | )
249 | }
250 |
251 | export default injectStripe(Checkout)
252 |
--------------------------------------------------------------------------------
/src/components/Error.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Field } from 'react-final-form'
3 |
4 | export default function Error({ name }) {
5 | return (
6 |
7 | {({ meta: { error }, ...rest }) => {
8 | console.log({ error })
9 | console.log({ rest })
10 |
11 | return error ? (
12 | {error}
13 | ) : null
14 | }}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Grid.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Grid = styled.div`
4 | display: flex;
5 | flex-wrap: wrap;
6 | margin-left: -0.75rem;
7 | margin-right: -0.75rem;
8 | `
9 |
10 | export const GridCol = styled.div`
11 | padding-left: 0.75rem;
12 | padding-right: 0.75rem;
13 | width: 50%;
14 | `
15 |
--------------------------------------------------------------------------------
/src/components/Input.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Field } from 'react-final-form'
3 | import styled from 'styled-components'
4 |
5 | import Label from './Label'
6 |
7 | export default function Input({
8 | label,
9 | name,
10 | autoFocus,
11 | type,
12 | placeholder,
13 | error: propsError,
14 | required,
15 | ...props
16 | }) {
17 | return (
18 |
19 | {({ meta, input, ...rest }) => {
20 | const error = propsError || (meta.error && meta.touched)
21 |
22 | return (
23 |
24 | {label && (
25 |
26 | {label}
27 |
28 | )}
29 |
30 |
39 |
40 | {!props.hideError && error && (
41 | {propsError || meta.error}
42 | )}
43 |
44 | )
45 | }}
46 |
47 | )
48 | }
49 |
50 | const InputGroup = styled.div`
51 | width: 100%;
52 | `
53 |
54 | const StyledInput = styled.input`
55 | background-color: ${props => props.theme.white};
56 | border-radius: 0.25rem;
57 | border: 1px solid
58 | ${props => (props.error ? props.theme.error : props.theme.border)};
59 | color: ${props => props.theme.dark};
60 | display: block;
61 | font-family: inherit;
62 | font-size: 100%;
63 | font-weight: 400;
64 | padding: 0.75rem 1rem;
65 | margin: 0;
66 | overflow: visible;
67 | outline: none;
68 | width: 100%;
69 |
70 | &::-webkit-input-placeholder {
71 | color: ${props => props.theme.placeholder};
72 | }
73 | &::-moz-placeholder {
74 | color: ${props => props.theme.placeholder};
75 | }
76 | &:-ms-input-placeholder {
77 | color: ${props => props.theme.placeholder};
78 | }
79 | &:-moz-placeholder {
80 | color: ${props => props.theme.placeholder};
81 | }
82 |
83 | &:hover {
84 | cursor: text;
85 | }
86 | `
87 |
88 | export const ErrorAlert = styled.span`
89 | color: ${props => props.theme.error};
90 | display: inline-block;
91 | padding-top: 0.75rem;
92 | `
93 |
--------------------------------------------------------------------------------
/src/components/Label.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export default function Label({
5 | htmlFor,
6 | label,
7 | children,
8 | required,
9 | ...props
10 | }) {
11 | return (
12 |
13 | {children || label} {required && * }
14 |
15 | )
16 | }
17 |
18 | const StyledLabel = styled.label`
19 | display: inline-flex;
20 | // color: ${props => (props.error ? props.theme.error : props.theme.dark)};
21 | color: ${props => props.theme.dark};
22 | cursor: pointer;
23 | font-weight: 500;
24 | margin: 0.75rem 0;
25 | `
26 |
27 | const Required = styled.span`
28 | margin-left: 0.25rem;
29 | color: ${props => props.theme.placeholder};
30 | font-weight: 400;
31 | `
32 |
--------------------------------------------------------------------------------
/src/components/LoginButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useActions } from 'easy-peasy'
3 |
4 | import { PrimaryButton } from './Button'
5 |
6 | function LoginButton({ text }) {
7 | const { goToLogin } = useActions(({ modal }) => modal)
8 |
9 | return (
10 |
11 | {text}
12 |
13 | )
14 | }
15 |
16 | LoginButton.defaultProps = {
17 | text: 'Login'
18 | }
19 |
20 | export default LoginButton
21 |
--------------------------------------------------------------------------------
/src/components/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useActions } from 'easy-peasy'
3 | import { Form } from 'react-final-form'
4 |
5 | import { Heading } from './typography'
6 | import { RouteHeader } from './Modal/Header'
7 | import Input from './Input'
8 | import { PrimaryButton } from './Button'
9 | import { loginValidation } from '../validation/auth'
10 |
11 | function LoginForm() {
12 | const [error, setError] = useState(null)
13 | const { login } = useActions(({ user }) => user)
14 | const { goToOrders } = useActions(({ modal }) => modal)
15 |
16 | const onSubmit = async ({ email, password }) => {
17 | try {
18 | await login({ email, password })
19 | goToOrders()
20 | } catch (err) {
21 | setError(err.message)
22 | }
23 | }
24 |
25 | return (
26 |
27 |
28 | Login
29 |
30 |
31 |
54 | )
55 | }}
56 |
57 |
58 | )
59 | }
60 |
61 | export default LoginForm
62 |
--------------------------------------------------------------------------------
/src/components/Modal/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { useStore, useActions } from 'easy-peasy'
4 |
5 | import PoweredBy from '../PoweredBy'
6 | import { Text, Link } from '../typography'
7 |
8 | function ModalFooter() {
9 | const { route } = useStore(({ modal }) => modal)
10 | const { goToLogin } = useActions(({ modal }) => modal)
11 |
12 | return (
13 |
14 | {/*
15 |
16 | Already have an account?
17 |
18 |
19 | Login to your account
20 |
21 |
22 | */}
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | const Wrapper = styled.footer`
30 | margin-top: 1.5rem;
31 | `
32 |
33 | const GuestCTA = styled.div`
34 | text-align: center;
35 | margin: 1.5rem;
36 |
37 | a {
38 | font-weight: 500;
39 | }
40 | `
41 |
42 | export default ModalFooter
43 |
--------------------------------------------------------------------------------
/src/components/Modal/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useActions, useStore } from 'easy-peasy'
3 | import styled from 'styled-components'
4 |
5 | import Button from '../Button'
6 |
7 | export default function Header({ route }) {
8 | const { closeModal, goToCart, goToShipping } = useActions(
9 | ({ modal }) => modal
10 | )
11 | const { dirty, completed } = useStore(({ checkout }) => checkout)
12 | const { loggedIn } = useStore(({ user }) => user)
13 |
14 | const handleClick = async () => {
15 | switch (route) {
16 | case 'billing':
17 | if (completed) {
18 | return closeModal()
19 | }
20 |
21 | return goToShipping()
22 |
23 | case 'shipping': {
24 | if (!completed && dirty) {
25 | const proceed = confirm(
26 | 'Are you sure you want to abandon your checkout?'
27 | )
28 |
29 | if (!proceed) return false
30 | }
31 |
32 | return goToCart()
33 | }
34 |
35 | default:
36 | return closeModal()
37 | }
38 | }
39 |
40 | return (
41 |
42 |
43 | {route === 'shipping' || (route === 'billing' && !completed) ? (
44 |
49 |
50 |
54 |
55 |
60 |
61 | ) : (
62 |
67 |
68 |
72 |
73 |
78 |
79 | )}
80 |
81 |
82 | )
83 | }
84 |
85 | const StyledHeader = styled.header`
86 | display: flex;
87 | align-items: center;
88 | justify-content: space-between;
89 | `
90 |
91 | const ActionButton = styled(Button)`
92 | background-color: transparent;
93 | padding: 0.25rem;
94 | `
95 |
96 | const SVG = styled.svg`
97 | fill: currentColor;
98 | width: 12px;
99 | height: 12px;
100 | `
101 |
102 | export const RouteHeader = styled.div`
103 | text-align: center;
104 | margin-bottom: 1.5rem;
105 | `
106 |
--------------------------------------------------------------------------------
/src/components/Modal/index.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react'
2 | import { useStore, useActions } from 'easy-peasy'
3 | import { StripeProvider, Elements } from 'react-stripe-elements'
4 | import styled from 'styled-components'
5 |
6 | import useOnClickOutside from '../../hooks/useOnClickOutside'
7 | import useScript from '../../hooks/useScript'
8 |
9 | import Header from './Header'
10 | import Footer from './Footer'
11 | import LoginForm from '../LoginForm'
12 | import OrderList from '../OrderList'
13 | import Cart from '../Cart'
14 | import Checkout from '../Checkout'
15 |
16 | function renderRoute(route) {
17 | switch (route) {
18 | case 'login':
19 | return
20 |
21 | case 'orders': {
22 | return
23 | }
24 |
25 | case 'shipping':
26 | case 'billing':
27 | return (
28 |
29 |
30 |
31 | )
32 |
33 | case 'cart':
34 | default:
35 | return
36 | }
37 | }
38 |
39 | export default function Modal({ stripeKey }) {
40 | const { open, route } = useStore(({ modal }) => modal)
41 | const { closeModal } = useActions(({ modal }) => modal)
42 | const [stripeLoaded, stripeError] = useScript('https://js.stripe.com/v3')
43 |
44 | const ref = useRef()
45 |
46 | useOnClickOutside(ref, closeModal, open)
47 |
48 | if (stripeError) {
49 | console.error(stripeError)
50 | return null
51 | }
52 |
53 | if (stripeLoaded && !stripeError) {
54 | return (
55 |
56 |
57 |
58 |
59 |
60 | {renderRoute(route)}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | )
70 | }
71 |
72 | return null
73 | }
74 |
75 | const StyledModal = styled.div.attrs({
76 | className: 'moltin-shopkit shopkit-modal'
77 | })`
78 | transition: all 0.3s ease;
79 | background-color: #fff;
80 | position: fixed;
81 | top: 0;
82 | bottom: 0;
83 | right: 0;
84 | overflow-y: scroll;
85 | height: 100%;
86 | box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
87 | z-index: 1000000001;
88 | padding: 1.5rem;
89 | width: 100%;
90 | // font-size: 0.9375rem;
91 | border-width: 0;
92 | max-width: 500px;
93 | opacity: ${props => (props.open ? 1 : 0)};
94 | visibility: ${props => (props.open ? 'visible' : 'hidden')};
95 | transform: translateX(${props => (props.open ? 0 : '525px')});
96 | display: flex;
97 | flex-direction: column;
98 | justify-content: space-between;
99 |
100 | box-sizing: border-box;
101 | line-height: 1.15;
102 | -webkit-text-size-adjust: 100%;
103 | margin: 0;
104 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
105 | Helvetica Neue, Arial, Noto Sans, sans-serif;
106 | font-size: 15px;
107 |
108 | *,
109 | *::before,
110 | *::after {
111 | box-sizing: inherit;
112 | -webkit-font-smoothing: antialiased;
113 | }
114 | `
115 |
116 | const ModalOverlay = styled.div.attrs({
117 | className: 'shopkit-modal-overlay'
118 | })`
119 | transition: all 0.3s ease;
120 | background-color: #333;
121 | position: fixed;
122 | top: 0;
123 | right: 0;
124 | bottom: 0;
125 | left: 0;
126 | z-index: 1000000000;
127 | opacity: ${props => (props.open ? 0.3 : 0)};
128 | visibility: ${props => (props.open ? 'visible' : 'hidden')};
129 | overflow-x: ${props => (props.open ? 100 : 0)};
130 | `
131 |
--------------------------------------------------------------------------------
/src/components/OrderConfirmation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useActions } from 'easy-peasy'
3 | import styled, { keyframes } from 'styled-components'
4 |
5 | import { Heading, Text } from './typography'
6 | import { PrimaryButton } from './Button'
7 |
8 | const stroke = keyframes`
9 | 100% {
10 | stroke-dashoffset: 300;
11 | }
12 | `
13 |
14 | const scale = keyframes`
15 | 0%, 100% {
16 | transform: none;
17 | }
18 | 50% {
19 | transform: scale3d(0.9, 0.9, 1);
20 | }
21 | `
22 |
23 | const fadeIn = keyframes`
24 | 0% {
25 | opacity: 0;
26 | }
27 | 100% {
28 | opacity: 1;
29 | }
30 | `
31 |
32 | const curve = `cubic-bezier(0.650, 0.000, 0.450, 1.000)`
33 |
34 | const Wrapper = styled.div`
35 | text-align: center;
36 | `
37 |
38 | const AnimatedHeading = styled(Heading)`
39 | animation: ${fadeIn} 0.3s;
40 | margin-bottom: 0.5rem;
41 | `
42 |
43 | const AnimatedText = styled(Text)`
44 | animation: ${fadeIn} 0.3s;
45 | margin-bottom: 1rem;
46 | `
47 |
48 | const AnimatedButton = styled(PrimaryButton)`
49 | animation: ${fadeIn} 0.3s;
50 | `
51 |
52 | const Tick = styled.div`
53 | width: 80px;
54 | height: 80px;
55 | margin: 3rem auto;
56 | animation: ${fadeIn} 0.3s;
57 |
58 | .success {
59 | display: block;
60 | stroke-width: 2;
61 | stroke: #fff;
62 | animation: ${scale} 0.3s ease-in-out 0.8s both;
63 | color: ${props => props.theme.primary};
64 | }
65 |
66 | .circle {
67 | stroke-dasharray: 800;
68 | stroke-dashoffset: 800;
69 | stroke-width: 2;
70 | stroke: currentColor;
71 | fill: none;
72 | animation: ${stroke} 0.5s ${curve} forwards;
73 | }
74 |
75 | .checkmark {
76 | transform-origin: 50% 50%;
77 | stroke-dasharray: 146;
78 | stroke-dashoffset: 146;
79 | animation: ${stroke} 0.2s ${curve} 0.9s forwards;
80 | stroke: currentColor;
81 | }
82 | `
83 |
84 | export default function({}) {
85 | const { continueShopping } = useActions(({ modal }) => modal)
86 |
87 | return (
88 |
89 |
90 |
97 |
98 |
99 |
106 |
107 |
108 |
109 |
110 | Order confirmed!
111 |
112 | Thank you for your order.
113 |
114 |
115 | Continue shopping
116 |
117 |
118 | {/*
119 |
Order summary
120 |
Items
121 |
122 | Total
123 |
124 | {meta.display_price.with_tax.formatted}
125 |
126 |
127 |
*/}
128 |
129 | )
130 | }
131 |
--------------------------------------------------------------------------------
/src/components/OrderItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled, { css } from 'styled-components'
3 |
4 | import Button from './Button'
5 |
6 | const Badge = styled.span`
7 | background-color: ${props => props.theme.success};
8 | color: ${props => props.theme.white};
9 | font-size: ${props => props.theme.textSmall};
10 | font-weight: 500;
11 | padding: 0.3rem 0.5rem;
12 | border-radius: 0.25rem;
13 |
14 | ${({ status }) =>
15 | status === 'cancelled' &&
16 | css`
17 | background-color: ${props => props.theme.error};
18 | `};
19 | `
20 |
21 | const Wrapper = styled.div`
22 | border: 1px solid ${props => props.theme.divider};
23 | border-radius: 0.25rem;
24 | margin: 1.5rem 0;
25 |
26 | &:hover {
27 | border-color: ${props => props.theme.border};
28 |
29 | button {
30 | border-bottom-color: ${props => props.theme.border};
31 | }
32 | }
33 | `
34 |
35 | const StatusBar = styled(Button)`
36 | align-items: center;
37 | border-bottom: 1px solid ${props => props.theme.divider};
38 | border-bottom-left-radius: 0;
39 | border-bottom-right-radius: 0;
40 | display: flex;
41 | justify-content: space-between;
42 | padding: 1rem 1rem;
43 | `
44 |
45 | const Summary = styled.div`
46 | padding: 1.25rem 1rem;
47 | display: flex;
48 | align-items: start;
49 | justify-content: space-between;
50 | `
51 |
52 | const StyledDate = styled.span`
53 | color: ${props => props.theme.dark};
54 | font-size: ${props => props.theme.textBase};
55 | font-weight: 700;
56 | margin: 0;
57 | display: flex;
58 | align-items: center;
59 |
60 | svg {
61 | margin-left: 0.75rem;
62 | fill: ${props => props.theme.placeholder};
63 | height: 12px;
64 | display: inline-block;
65 | }
66 | `
67 |
68 | function OrderItem({ id, status, meta }) {
69 | const {
70 | timestamps: { created_at },
71 | display_price
72 | } = meta
73 | const formattedDate = new Date(created_at).toLocaleDateString()
74 |
75 | return (
76 |
77 | console.log(id)}>
78 |
79 | {formattedDate}
80 |
86 |
91 |
92 |
93 | {status}
94 |
95 |
96 |
97 |
Total
98 |
{display_price.with_tax.formatted}
99 |
100 |
101 |
Items
102 |
3
103 |
104 |
105 |
106 | )
107 | }
108 |
109 | export default OrderItem
110 |
--------------------------------------------------------------------------------
/src/components/OrderList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { useStore, useActions } from 'easy-peasy'
3 |
4 | import { Heading } from './typography'
5 | import { RouteHeader } from './Modal/Header'
6 | import OrderItem from './OrderItem'
7 |
8 | function OrdersList() {
9 | const { orders } = useStore(({ user }) => user)
10 | const { getOrders } = useActions(({ user }) => user)
11 |
12 | useEffect(() => {
13 | getOrders()
14 | }, [])
15 |
16 | return (
17 |
18 |
19 | Your orders
20 |
21 |
22 |
23 | {orders.map(order => (
24 |
25 | ))}
26 |
27 |
28 | )
29 | }
30 |
31 | export default OrdersList
32 |
--------------------------------------------------------------------------------
/src/components/PlacesSuggest.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AlgoliaPlaces from 'algolia-places-react'
3 | import styled from 'styled-components'
4 |
5 | import Label from './Label'
6 |
7 | const options = {
8 | appId: 'plEPUZAA2D2L',
9 | apiKey: '4c9f0832a65f800e31b0d50f44670b1f',
10 | type: ['city', 'address'],
11 | useDeviceLocation: false,
12 | style: false
13 | }
14 |
15 | export default function PlacesSuggest({ label, ...props }) {
16 | const id = `${label}_suggest`
17 |
18 | return (
19 |
20 | {label && Search for your {label} address }
21 |
22 |
29 |
30 | )
31 | }
32 |
33 | const AlgoliaSearch = styled.div`
34 | .algolia-places-nostyle {
35 | width: 100%;
36 | }
37 |
38 | .ap-nostyle-input {
39 | background-color: ${props => props.theme.white};
40 | border-radius: 0.25rem;
41 | border: 1px solid ${props => props.theme.border};
42 | color: ${props => props.theme.dark};
43 | display: block;
44 | font-family: inherit;
45 | font-size: 100%;
46 | font-weight: 400;
47 | padding: 0.75rem 1rem;
48 | margin: 0;
49 | overflow: visible;
50 | outline: none;
51 | width: 100%;
52 |
53 | &::-webkit-input-placeholder {
54 | color: ${props => props.theme.placeholder};
55 | }
56 | &::-moz-placeholder {
57 | color: ${props => props.theme.placeholder};
58 | }
59 | &:-ms-input-placeholder {
60 | color: ${props => props.theme.placeholder};
61 | }
62 | &:-moz-placeholder {
63 | color: ${props => props.theme.placeholder};
64 | }
65 |
66 | &:hover {
67 | cursor: text;
68 | }
69 | }
70 |
71 | .ap-nostyle-icon-pin,
72 | .ap-nostyle-input-icon,
73 | .ap-nostyle-suggestion-icon,
74 | .ap-suggestion-icon {
75 | display: none;
76 | }
77 |
78 | .ap-nostyle-dropdown-menu {
79 | background-color: ${props => props.theme.white};
80 | border-radius: 0.25rem;
81 | border: 1px solid ${props => props.theme.border};
82 | margin-top: 0.5rem;
83 | width: 100%;
84 | overflow: hidden;
85 | }
86 |
87 | .ap-nostyle-suggestion,
88 | .ap-suggestion {
89 | padding: 0.75rem 1rem;
90 | color: ${props => props.theme.placeholder};
91 | font-size: ${props => props.theme.textSmall};
92 | font-style: normal;
93 | font-weight: 500;
94 | border-bottom: 1px solid ${props => props.theme.cursor};
95 | line-height: 1.5;
96 | }
97 |
98 | .ap-nostyle-suggestion em,
99 | .ap-suggestion em {
100 | font-style: normal;
101 | color: ${props => props.theme.primary};
102 | }
103 |
104 | .ap-nostyle-cursor,
105 | .ap-cursor {
106 | background-color: ${props => props.theme.cursor};
107 | }
108 |
109 | .ap-nostyle-name,
110 | .ap-name {
111 | color: ${props => props.theme.dark};
112 | font-size: ${props => props.theme.textSmall};
113 | font-weight: 500;
114 | }
115 |
116 | .ap-nostyle-address,
117 | .ap-address {
118 | color: ${props => props.theme.placeholder};
119 | font-size: ${props => props.theme.textSmall};
120 | margin-left: 0.5rem;
121 | }
122 |
123 | .ap-nostyle-input-icon.ap-nostyle-icon-clear {
124 | background-color: ${props => props.theme.white};
125 | appearance: none;
126 | border: 0;
127 | cursor: pointer;
128 | outline: none;
129 | position: absolute;
130 | right: 0;
131 | top: 0;
132 | bottom: 0;
133 | display: flex;
134 | align-items: center;
135 | padding: 0 0.75rem;
136 | margin-top: 0.5rem;
137 | margin-right: 1px;
138 | margin-bottom: 0.5rem;
139 | color: ${props => props.theme.placeholder};
140 | z-index: 5;
141 |
142 | svg {
143 | fill: currentColor;
144 | width: 10px;
145 | height: 10px;
146 | }
147 |
148 | &:hover,
149 | &:focus {
150 | outline: none;
151 | }
152 |
153 | &::-moz-focus-inner {
154 | border-style: none;
155 | padding: 0;
156 | }
157 | }
158 | `
159 |
--------------------------------------------------------------------------------
/src/components/PoweredBy.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | const Wrapper = styled.div`
5 | align-items: center;
6 | display: flex;
7 | `
8 |
9 | const Link = styled.a`
10 | cursor: pointer;
11 | display: inline-flex;
12 | align-items: center;
13 | font-family: inherit;
14 | font-size: 100%;
15 | text-decoration: none;
16 | margin: 0 auto;
17 | // opacity: 0.3;
18 | transition: opacity 0.1s ease-in;
19 |
20 | // &:hover {
21 | // opacity: 1;
22 | // }
23 | `
24 |
25 | const Logo = styled.svg`
26 | height: 2rem;
27 | `
28 |
29 | const Text = styled.span`
30 | margin-right: 0.5rem;
31 | color: ${props => props.theme.placeholder};
32 | font-size: ${props => props.theme.textSmall};
33 | `
34 |
35 | export default function PoweredBy() {
36 | return (
37 |
38 |
42 | Powered by
43 |
44 |
51 |
55 |
59 |
63 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/PromotionManager/Form.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Form } from 'react-final-form'
3 | import styled from 'styled-components'
4 |
5 | import Label from '../Label'
6 | import Input from '../Input'
7 | import { PrimaryButton } from '../Button'
8 |
9 | export default function PromotionForm({ addPromotion }) {
10 | const [error, setError] = useState(null)
11 |
12 | const onSubmit = async ({ code }) => {
13 | try {
14 | await addPromotion(code)
15 | } catch (err) {
16 | setError(err.message)
17 | }
18 | }
19 |
20 | return (
21 |
42 | )}
43 |
44 | )
45 | }
46 |
47 | const Row = styled.div`
48 | align-items: start;
49 | display: flex;
50 | width: 100%;
51 | `
52 |
53 | const StyledButton = styled(PrimaryButton)`
54 | margin-left: 0.5rem;
55 | `
56 |
--------------------------------------------------------------------------------
/src/components/PromotionManager/Promotion.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import Label from '../Label'
5 | import { PrimaryButton } from '../Button'
6 |
7 | export default function Promotion({ id, sku, removePromotion }) {
8 | return (
9 |
10 |
Gift card or discount code
11 |
12 |
13 | {sku}
14 | removePromotion(id)}>
15 | Remove
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | const Wrapper = styled.div`
23 | display: flex;
24 | align-items: center;
25 | justify-content: space-between;
26 | `
27 |
28 | const SKU = styled.span`
29 | color: ${props => props.theme.dark};
30 | `
31 |
--------------------------------------------------------------------------------
/src/components/PromotionManager/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import Promotion from './Promotion'
5 | import Form from './Form'
6 |
7 | export default function PromotionManager({
8 | promotionItems,
9 | addPromotion,
10 | removePromotion
11 | }) {
12 | const hasPromotion = !!promotionItems.length
13 | const [promotion] = promotionItems
14 |
15 | return (
16 |
17 | {hasPromotion ? (
18 |
19 | ) : (
20 |
21 | )}
22 |
23 | )
24 | }
25 |
26 | const Wrapper = styled.div`
27 | margin: 0;
28 | padding: 0.75rem 0 1.5rem;
29 | border-top: 1px solid ${props => props.theme.divider};
30 | `
31 |
--------------------------------------------------------------------------------
/src/components/QuantityStepper.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import Button from './Button'
5 |
6 | const Wrapper = styled.div`
7 | display: flex;
8 | justify-content: space-around;
9 | align-items: center;
10 | flex-direction: column;
11 | `
12 |
13 | const Quantity = styled.span`
14 | color: ${props => props.theme.dark};
15 | font-weight: 500;
16 | padding: 0.25rem 0;
17 | `
18 |
19 | const SVG = styled.svg`
20 | stroke: currentColor;
21 | width: 10px;
22 | height: 10px;
23 | `
24 |
25 | export default function QuantityStepper({ itemId, quantity, updateItem }) {
26 | const increase = () => updateItem({ id: itemId, quantity: quantity + 1 })
27 | const decrease = () => updateItem({ id: itemId, quantity: quantity - 1 })
28 |
29 | return (
30 |
31 |
32 |
38 |
46 |
47 |
48 | {quantity}
49 |
50 |
56 |
64 |
65 |
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/Select.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Field } from 'react-final-form'
3 | import styled from 'styled-components'
4 |
5 | import Label from './Label'
6 | import { ErrorAlert } from './Input'
7 |
8 | const SelectGroup = styled.div`
9 | width: 100%;
10 | `
11 |
12 | const SelectWrapper = styled.div`
13 | background-color: ${props => props.theme.white};
14 | border: 1px solid
15 | ${props => (props.error ? props.theme.error : props.theme.border)};
16 | border-radius: 0.25rem;
17 | position: relative;
18 | `
19 |
20 | const StyledSelect = styled.select`
21 | appearance: none;
22 | background-color: ${props => props.theme.white};
23 | border: 0;
24 | color: ${props => props.theme.dark};
25 | display: block;
26 | font-family: inherit;
27 | font-size: 100%;
28 | font-weight: 400;
29 | padding: 0.75rem 1.5rem 0.75rem 1rem;
30 | margin: 0;
31 | overflow: visible;
32 | outline: none;
33 | width: 100%;
34 |
35 | &::-webkit-input-placeholder {
36 | color: ${props => props.theme.placeholder};
37 | }
38 | &::-moz-placeholder {
39 | color: ${props => props.theme.placeholder};
40 | }
41 | &:-ms-input-placeholder {
42 | color: ${props => props.theme.placeholder};
43 | }
44 | &:-moz-placeholder {
45 | color: ${props => props.theme.placeholder};
46 | }
47 |
48 | &:hover {
49 | cursor: pointer;
50 | }
51 |
52 | &:focus,
53 | &:active {
54 | outline: none;
55 | }
56 | `
57 |
58 | const Icon = styled.div`
59 | align-items: center;
60 | background-color: ${props => props.theme.white};
61 | bottom: 0;
62 | border-left: 1px solid ${props => props.theme.border};
63 | color: ${props => props.theme.placeholder};
64 | display: flex;
65 | padding: 0 0.75rem;
66 | position: absolute;
67 | pointer-events: none;
68 | right: 0;
69 | top: 0;
70 | margin-top: 0.5rem;
71 | margin-bottom: 0.5rem;
72 | `
73 |
74 | const SVG = styled.svg`
75 | fill: currentColor;
76 | height: 12px;
77 | `
78 |
79 | const CustomSelect = ({ label, input, meta, options, required }) => {
80 | const error = meta.error && meta.touched
81 |
82 | return (
83 |
84 | {label && (
85 |
86 | {label}
87 |
88 | )}
89 |
90 |
91 |
92 | Select
93 | {options.map(opt => (
94 |
95 | {opt.label || opt.value}
96 |
97 | ))}
98 |
99 |
100 |
101 |
106 |
107 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | {error && {meta.error} }
118 |
119 | )
120 | }
121 |
122 | export default function FieldSelect({ name, options, ...rest }) {
123 | return (
124 |
125 | )
126 | }
127 |
--------------------------------------------------------------------------------
/src/components/ShopkitIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | const ShopkitIcon = () => (
5 |
6 |
10 |
11 | )
12 |
13 | const Icon = styled.svg`
14 | height: 2.5rem;
15 | fill: ${props => props.theme.border};
16 | `
17 |
18 | export default ShopkitIcon
19 |
--------------------------------------------------------------------------------
/src/components/typography.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Heading = styled.h2`
4 | color: ${props => props.theme.dark};
5 | font-weight: 700;
6 | font-size: ${props => props.theme.textLarge};
7 | margin: 0;
8 | `
9 |
10 | export const Text = styled.p`
11 | color: ${props => props.theme.placeholder};
12 | margin: 0;
13 | line-height: 1.5;
14 | `
15 |
16 | export const Link = styled.a`
17 | color: ${props => props.theme.dark};
18 | text-decoration: underline;
19 |
20 | &:hover {
21 | color: ${props => props.theme.primary};
22 | }
23 | `
24 |
--------------------------------------------------------------------------------
/src/countries.js:
--------------------------------------------------------------------------------
1 | export default [
2 | { label: 'Afghanistan', value: 'AF' },
3 | { label: 'Åland Islands', value: 'AX' },
4 | { label: 'Albania', value: 'AL' },
5 | { label: 'Algeria', value: 'DZ' },
6 | { label: 'American Samoa', value: 'AS' },
7 | { label: 'AndorrA', value: 'AD' },
8 | { label: 'Angola', value: 'AO' },
9 | { label: 'Anguilla', value: 'AI' },
10 | { label: 'Antarctica', value: 'AQ' },
11 | { label: 'Antigua and Barbuda', value: 'AG' },
12 | { label: 'Argentina', value: 'AR' },
13 | { label: 'Armenia', value: 'AM' },
14 | { label: 'Aruba', value: 'AW' },
15 | { label: 'Australia', value: 'AU' },
16 | { label: 'Austria', value: 'AT' },
17 | { label: 'Azerbaijan', value: 'AZ' },
18 | { label: 'Bahamas', value: 'BS' },
19 | { label: 'Bahrain', value: 'BH' },
20 | { label: 'Bangladesh', value: 'BD' },
21 | { label: 'Barbados', value: 'BB' },
22 | { label: 'Belarus', value: 'BY' },
23 | { label: 'Belgium', value: 'BE' },
24 | { label: 'Belize', value: 'BZ' },
25 | { label: 'Benin', value: 'BJ' },
26 | { label: 'Bermuda', value: 'BM' },
27 | { label: 'Bhutan', value: 'BT' },
28 | { label: 'Bolivia', value: 'BO' },
29 | { label: 'Bosnia and Herzegovina', value: 'BA' },
30 | { label: 'Botswana', value: 'BW' },
31 | { label: 'Bouvet Island', value: 'BV' },
32 | { label: 'Brazil', value: 'BR' },
33 | { label: 'British Indian Ocean Territory', value: 'IO' },
34 | { label: 'Brunei Darussalam', value: 'BN' },
35 | { label: 'Bulgaria', value: 'BG' },
36 | { label: 'Burkina Faso', value: 'BF' },
37 | { label: 'Burundi', value: 'BI' },
38 | { label: 'Cambodia', value: 'KH' },
39 | { label: 'Cameroon', value: 'CM' },
40 | { label: 'Canada', value: 'CA' },
41 | { label: 'Cape Verde', value: 'CV' },
42 | { label: 'Cayman Islands', value: 'KY' },
43 | { label: 'Central African Republic', value: 'CF' },
44 | { label: 'Chad', value: 'TD' },
45 | { label: 'Chile', value: 'CL' },
46 | { label: 'China', value: 'CN' },
47 | { label: 'Christmas Island', value: 'CX' },
48 | { label: 'Cocos (Keeling) Islands', value: 'CC' },
49 | { label: 'Colombia', value: 'CO' },
50 | { label: 'Comoros', value: 'KM' },
51 | { label: 'Congo', value: 'CG' },
52 | { label: 'Congo, The Democratic Republic of the', value: 'CD' },
53 | { label: 'Cook Islands', value: 'CK' },
54 | { label: 'Costa Rica', value: 'CR' },
55 | { label: "Cote D'Ivoire", value: 'CI' },
56 | { label: 'Croatia', value: 'HR' },
57 | { label: 'Cuba', value: 'CU' },
58 | { label: 'Cyprus', value: 'CY' },
59 | { label: 'Czech Republic', value: 'CZ' },
60 | { label: 'Denmark', value: 'DK' },
61 | { label: 'Djibouti', value: 'DJ' },
62 | { label: 'Dominica', value: 'DM' },
63 | { label: 'Dominican Republic', value: 'DO' },
64 | { label: 'Ecuador', value: 'EC' },
65 | { label: 'Egypt', value: 'EG' },
66 | { label: 'El Salvador', value: 'SV' },
67 | { label: 'Equatorial Guinea', value: 'GQ' },
68 | { label: 'Eritrea', value: 'ER' },
69 | { label: 'Estonia', value: 'EE' },
70 | { label: 'Ethiopia', value: 'ET' },
71 | { label: 'Falkland Islands (Malvinas)', value: 'FK' },
72 | { label: 'Faroe Islands', value: 'FO' },
73 | { label: 'Fiji', value: 'FJ' },
74 | { label: 'Finland', value: 'FI' },
75 | { label: 'France', value: 'FR' },
76 | { label: 'French Guiana', value: 'GF' },
77 | { label: 'French Polynesia', value: 'PF' },
78 | { label: 'French Southern Territories', value: 'TF' },
79 | { label: 'Gabon', value: 'GA' },
80 | { label: 'Gambia', value: 'GM' },
81 | { label: 'Georgia', value: 'GE' },
82 | { label: 'Germany', value: 'DE' },
83 | { label: 'Ghana', value: 'GH' },
84 | { label: 'Gibraltar', value: 'GI' },
85 | { label: 'Greece', value: 'GR' },
86 | { label: 'Greenland', value: 'GL' },
87 | { label: 'Grenada', value: 'GD' },
88 | { label: 'Guadeloupe', value: 'GP' },
89 | { label: 'Guam', value: 'GU' },
90 | { label: 'Guatemala', value: 'GT' },
91 | { label: 'Guernsey', value: 'GG' },
92 | { label: 'Guinea', value: 'GN' },
93 | { label: 'Guinea-Bissau', value: 'GW' },
94 | { label: 'Guyana', value: 'GY' },
95 | { label: 'Haiti', value: 'HT' },
96 | { label: 'Heard Island and Mcdonald Islands', value: 'HM' },
97 | { label: 'Holy See (Vatican City State)', value: 'VA' },
98 | { label: 'Honduras', value: 'HN' },
99 | { label: 'Hong Kong', value: 'HK' },
100 | { label: 'Hungary', value: 'HU' },
101 | { label: 'Iceland', value: 'IS' },
102 | { label: 'India', value: 'IN' },
103 | { label: 'Indonesia', value: 'ID' },
104 | { label: 'Iran, Islamic Republic Of', value: 'IR' },
105 | { label: 'Iraq', value: 'IQ' },
106 | { label: 'Ireland', value: 'IE' },
107 | { label: 'Isle of Man', value: 'IM' },
108 | { label: 'Israel', value: 'IL' },
109 | { label: 'Italy', value: 'IT' },
110 | { label: 'Jamaica', value: 'JM' },
111 | { label: 'Japan', value: 'JP' },
112 | { label: 'Jersey', value: 'JE' },
113 | { label: 'Jordan', value: 'JO' },
114 | { label: 'Kazakhstan', value: 'KZ' },
115 | { label: 'Kenya', value: 'KE' },
116 | { label: 'Kiribati', value: 'KI' },
117 | { label: "Korea, Democratic People's Republic of", value: 'KP' },
118 | { label: 'Korea, Republic of', value: 'KR' },
119 | { label: 'Kuwait', value: 'KW' },
120 | { label: 'Kyrgyzstan', value: 'KG' },
121 | { label: "Lao People's Democratic Republic", value: 'LA' },
122 | { label: 'Latvia', value: 'LV' },
123 | { label: 'Lebanon', value: 'LB' },
124 | { label: 'Lesotho', value: 'LS' },
125 | { label: 'Liberia', value: 'LR' },
126 | { label: 'Libyan Arab Jamahiriya', value: 'LY' },
127 | { label: 'Liechtenstein', value: 'LI' },
128 | { label: 'Lithuania', value: 'LT' },
129 | { label: 'Luxembourg', value: 'LU' },
130 | { label: 'Macao', value: 'MO' },
131 | { label: 'Macedonia, The Former Yugoslav Republic of', value: 'MK' },
132 | { label: 'Madagascar', value: 'MG' },
133 | { label: 'Malawi', value: 'MW' },
134 | { label: 'Malaysia', value: 'MY' },
135 | { label: 'Maldives', value: 'MV' },
136 | { label: 'Mali', value: 'ML' },
137 | { label: 'Malta', value: 'MT' },
138 | { label: 'Marshall Islands', value: 'MH' },
139 | { label: 'Martinique', value: 'MQ' },
140 | { label: 'Mauritania', value: 'MR' },
141 | { label: 'Mauritius', value: 'MU' },
142 | { label: 'Mayotte', value: 'YT' },
143 | { label: 'Mexico', value: 'MX' },
144 | { label: 'Micronesia, Federated States of', value: 'FM' },
145 | { label: 'Moldova, Republic of', value: 'MD' },
146 | { label: 'Monaco', value: 'MC' },
147 | { label: 'Mongolia', value: 'MN' },
148 | { label: 'Montserrat', value: 'MS' },
149 | { label: 'Morocco', value: 'MA' },
150 | { label: 'Mozambique', value: 'MZ' },
151 | { label: 'Myanmar', value: 'MM' },
152 | { label: 'Namibia', value: 'NA' },
153 | { label: 'Nauru', value: 'NR' },
154 | { label: 'Nepal', value: 'NP' },
155 | { label: 'Netherlands', value: 'NL' },
156 | { label: 'Netherlands Antilles', value: 'AN' },
157 | { label: 'New Caledonia', value: 'NC' },
158 | { label: 'New Zealand', value: 'NZ' },
159 | { label: 'Nicaragua', value: 'NI' },
160 | { label: 'Niger', value: 'NE' },
161 | { label: 'Nigeria', value: 'NG' },
162 | { label: 'Niue', value: 'NU' },
163 | { label: 'Norfolk Island', value: 'NF' },
164 | { label: 'Northern Mariana Islands', value: 'MP' },
165 | { label: 'Norway', value: 'NO' },
166 | { label: 'Oman', value: 'OM' },
167 | { label: 'Pakistan', value: 'PK' },
168 | { label: 'Palau', value: 'PW' },
169 | { label: 'Palestinian Territory, Occupied', value: 'PS' },
170 | { label: 'Panama', value: 'PA' },
171 | { label: 'Papua New Guinea', value: 'PG' },
172 | { label: 'Paraguay', value: 'PY' },
173 | { label: 'Peru', value: 'PE' },
174 | { label: 'Philippines', value: 'PH' },
175 | { label: 'Pitcairn', value: 'PN' },
176 | { label: 'Poland', value: 'PL' },
177 | { label: 'Portugal', value: 'PT' },
178 | { label: 'Puerto Rico', value: 'PR' },
179 | { label: 'Qatar', value: 'QA' },
180 | { label: 'Reunion', value: 'RE' },
181 | { label: 'Romania', value: 'RO' },
182 | { label: 'Russian Federation', value: 'RU' },
183 | { label: 'RWANDA', value: 'RW' },
184 | { label: 'Saint Helena', value: 'SH' },
185 | { label: 'Saint Kitts and Nevis', value: 'KN' },
186 | { label: 'Saint Lucia', value: 'LC' },
187 | { label: 'Saint Pierre and Miquelon', value: 'PM' },
188 | { label: 'Saint Vincent and the Grenadines', value: 'VC' },
189 | { label: 'Samoa', value: 'WS' },
190 | { label: 'San Marino', value: 'SM' },
191 | { label: 'Sao Tome and Principe', value: 'ST' },
192 | { label: 'Saudi Arabia', value: 'SA' },
193 | { label: 'Senegal', value: 'SN' },
194 | { label: 'Serbia and Montenegro', value: 'CS' },
195 | { label: 'Seychelles', value: 'SC' },
196 | { label: 'Sierra Leone', value: 'SL' },
197 | { label: 'Singapore', value: 'SG' },
198 | { label: 'Slovakia', value: 'SK' },
199 | { label: 'Slovenia', value: 'SI' },
200 | { label: 'Solomon Islands', value: 'SB' },
201 | { label: 'Somalia', value: 'SO' },
202 | { label: 'South Africa', value: 'ZA' },
203 | { label: 'South Georgia and the South Sandwich Islands', value: 'GS' },
204 | { label: 'Spain', value: 'ES' },
205 | { label: 'Sri Lanka', value: 'LK' },
206 | { label: 'Sudan', value: 'SD' },
207 | { label: 'Suriname', value: 'SR' },
208 | { label: 'Svalbard and Jan Mayen', value: 'SJ' },
209 | { label: 'Swaziland', value: 'SZ' },
210 | { label: 'Sweden', value: 'SE' },
211 | { label: 'Switzerland', value: 'CH' },
212 | { label: 'Syrian Arab Republic', value: 'SY' },
213 | { label: 'Taiwan, Province of China', value: 'TW' },
214 | { label: 'Tajikistan', value: 'TJ' },
215 | { label: 'Tanzania, United Republic of', value: 'TZ' },
216 | { label: 'Thailand', value: 'TH' },
217 | { label: 'Timor-Leste', value: 'TL' },
218 | { label: 'Togo', value: 'TG' },
219 | { label: 'Tokelau', value: 'TK' },
220 | { label: 'Tonga', value: 'TO' },
221 | { label: 'Trinidad and Tobago', value: 'TT' },
222 | { label: 'Tunisia', value: 'TN' },
223 | { label: 'Turkey', value: 'TR' },
224 | { label: 'Turkmenistan', value: 'TM' },
225 | { label: 'Turks and Caicos Islands', value: 'TC' },
226 | { label: 'Tuvalu', value: 'TV' },
227 | { label: 'Uganda', value: 'UG' },
228 | { label: 'Ukraine', value: 'UA' },
229 | { label: 'United Arab Emirates', value: 'AE' },
230 | { label: 'United Kingdom', value: 'GB' },
231 | { label: 'United States', value: 'US' },
232 | { label: 'United States Minor Outlying Islands', value: 'UM' },
233 | { label: 'Uruguay', value: 'UY' },
234 | { label: 'Uzbekistan', value: 'UZ' },
235 | { label: 'Vanuatu', value: 'VU' },
236 | { label: 'Venezuela', value: 'VE' },
237 | { label: 'Viet Nam', value: 'VN' },
238 | { label: 'Virgin Islands, British', value: 'VG' },
239 | { label: 'Virgin Islands, U.S.', value: 'VI' },
240 | { label: 'Wallis and Futuna', value: 'WF' },
241 | { label: 'Western Sahara', value: 'EH' },
242 | { label: 'Yemen', value: 'YE' },
243 | { label: 'Zambia', value: 'ZM' },
244 | { label: 'Zimbabwe', value: 'ZW' }
245 | ]
246 |
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | export default function useLocalStorage(key, initialValue) {
4 | const [storedValue, setStoredValue] = useState(() => {
5 | try {
6 | const item = window.localStorage.getItem(key)
7 |
8 | return item ? item : initialValue
9 | } catch (error) {
10 | console.log(error)
11 | return initialValue
12 | }
13 | })
14 |
15 | const setValue = value => {
16 | try {
17 | const valueToStore =
18 | value instanceof Function ? value(storedValue) : value
19 |
20 | setStoredValue(valueToStore)
21 |
22 | window.localStorage.setItem(key, valueToStore)
23 | } catch (error) {
24 | console.log(error)
25 | }
26 | }
27 |
28 | return [storedValue, setValue]
29 | }
30 |
--------------------------------------------------------------------------------
/src/hooks/useOnClickOutside.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export default function useOnClickOutside(ref, handler, open) {
4 | const clickEvent = 'ontouchstart' in window ? 'touchstart' : 'mousedown'
5 |
6 | useEffect(() => {
7 | const listener = event => {
8 | if (ref.current.contains(event.target)) return
9 |
10 | handler(event)
11 | }
12 |
13 | if (open) {
14 | document.addEventListener(clickEvent, listener)
15 | } else {
16 | document.removeEventListener(clickEvent, listener)
17 | }
18 |
19 | return () => {
20 | document.removeEventListener(clickEvent, listener)
21 | }
22 | }, [ref, handler, open])
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/useScript.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | let cachedScripts = []
4 |
5 | export default function useScript(src) {
6 | const [state, setState] = useState({
7 | loaded: false,
8 | error: false
9 | })
10 |
11 | useEffect(() => {
12 | if (cachedScripts.includes(src)) {
13 | setState({
14 | loaded: true,
15 | error: false
16 | })
17 | } else {
18 | cachedScripts.push(src)
19 |
20 | let script = document.createElement('script')
21 | script.src = src
22 | script.async = true
23 |
24 | const onScriptLoad = () => {
25 | setState({
26 | loaded: true,
27 | error: false
28 | })
29 | }
30 |
31 | const onScriptError = () => {
32 | const index = cachedScripts.indexOf(src)
33 | if (index >= 0) cachedScripts.splice(index, 1)
34 | script.remove()
35 |
36 | setState({
37 | loaded: true,
38 | error: true
39 | })
40 | }
41 |
42 | script.addEventListener('load', onScriptLoad)
43 | script.addEventListener('error', onScriptError)
44 |
45 | document.body.appendChild(script)
46 |
47 | return () => {
48 | script.removeEventListener('load', onScriptLoad)
49 | script.removeEventListener('error', onScriptError)
50 | }
51 | }
52 | }, [src])
53 |
54 | return [state.loaded, state.error]
55 | }
56 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { MoltinClient } from '@moltin/request'
4 | import { createStore, StoreProvider } from 'easy-peasy'
5 | import { ThemeProvider } from 'styled-components'
6 | import 'arrive';
7 |
8 | import model from './model'
9 |
10 | import Modal from './components/Modal'
11 | import BuyButton from './components/BuyButton'
12 | import CartButton from './components/CartButton'
13 | import LoginButton from './components/LoginButton'
14 |
15 | import theme from './theme'
16 |
17 | function init(document) {
18 | const script = document.querySelector('script[data-moltin-client-id]')
19 |
20 | if (!script) {
21 | console.error(
22 | 'You must provide a Moltin Client ID to enable the Moltin Btn'
23 | )
24 | return
25 | }
26 |
27 | const {
28 | moltinClientId: client_id,
29 | moltinStripePublishableKey,
30 | moltinCurrency: currency,
31 | moltinEndpointUrl: host
32 | } = script.dataset
33 |
34 | if (!moltinStripePublishableKey) {
35 | console.error(
36 | 'You must provide your Stripe Publishable Key to enable the Moltin Btn'
37 | )
38 | return
39 | }
40 |
41 | const buttons = [...document.querySelectorAll('.moltin-buy-button')]
42 | const cartBtns = [...document.querySelectorAll('.moltin-cart-button')]
43 | const loginBtns = [...document.querySelectorAll('.moltin-login-button')]
44 |
45 | const cart = document.createElement('div')
46 | document.body.appendChild(cart)
47 |
48 | const api = new MoltinClient({
49 | client_id,
50 | application: 'moltin-btn',
51 | ...(currency && { currency }),
52 | ...(host && { host })
53 | })
54 |
55 | const store = createStore(model, {
56 | injections: {
57 | api
58 | }
59 | })
60 |
61 | document.arrive(
62 | ".moltin-buy-button",
63 | { existing: true },
64 | (el) => ReactDOM.render(
65 |
66 |
67 |
68 |
69 | ,
70 | el
71 | )
72 | );
73 |
74 | document.arrive(
75 | ".moltin-cart-button",
76 | { existing: true },
77 | (el) => ReactDOM.render(
78 |
79 |
80 |
81 |
82 | ,
83 | el
84 | )
85 | );
86 |
87 | document.arrive(
88 | ".moltin-login-button",
89 | { existing: true },
90 | (el) => ReactDOM.render(
91 |
92 |
93 |
94 |
95 | ,
96 | el
97 | )
98 | );
99 |
100 | ReactDOM.render(
101 |
102 |
103 |
104 |
105 | ,
106 | cart
107 | )
108 | }
109 |
110 | if (document.readyState === 'complete') {
111 | init(document)
112 | }
113 |
114 | window.addEventListener('load', function() {
115 | init(document)
116 | })
117 |
--------------------------------------------------------------------------------
/src/model/cart.js:
--------------------------------------------------------------------------------
1 | import { action, thunk, select } from 'easy-peasy'
2 |
3 | export default {
4 | id: null,
5 | meta: null,
6 | items: [],
7 |
8 | isEmpty: select(({ items }) => items.length === 0),
9 |
10 | count: select(({ items }) =>
11 | items.reduce((sum, { quantity }) => sum + quantity, 0)
12 | ),
13 |
14 | cartItems: select(({ items }) =>
15 | items.filter(({ type }) => type === 'cart_item' || type === 'custom_item')
16 | ),
17 |
18 | taxItems: select(({ items }) =>
19 | items.filter(({ type }) => type === 'tax_item')
20 | ),
21 |
22 | promotionItems: select(({ items }) =>
23 | items.filter(({ type }) => type === 'promotion_item')
24 | ),
25 |
26 | subTotal: select(({ meta }) =>
27 | meta ? meta.display_price.without_tax.formatted : 0
28 | ),
29 |
30 | setCartId: action((state, cartId) => {
31 | state.id = cartId
32 | }),
33 |
34 | setCart: action((state, { data, meta }) => {
35 | state.items = data
36 | state.meta = meta
37 | }),
38 |
39 | getCart: thunk(async (actions, id, { injections: { api } }) => {
40 | const payload = await api.get(`carts/${id}/items`)
41 |
42 | actions.setCart(payload)
43 | }),
44 |
45 | deleteCart: thunk(async (actions, id, { injections: { api } }) => {
46 | await api.delete(`carts/${id}`)
47 |
48 | actions.setCart({ data: [], meta: null })
49 | }),
50 |
51 | addToCart: thunk(
52 | async (
53 | actions,
54 | { quantity = 1, type = 'cart_item', ...rest },
55 | { getState, injections: { api } }
56 | ) => {
57 | const { id: cartId } = getState()
58 |
59 | const payload = await api.post(`carts/${cartId}/items`, {
60 | type,
61 | quantity,
62 | ...rest
63 | })
64 |
65 | actions.setCart(payload)
66 | }
67 | ),
68 |
69 | updateItem: thunk(
70 | async (actions, { id, quantity }, { getState, injections: { api } }) => {
71 | const { id: cartId } = getState()
72 |
73 | const payload = await api.put(`carts/${cartId}/items/${id}`, {
74 | type: 'cart_item',
75 | id,
76 | quantity
77 | })
78 |
79 | await actions.setCart(payload)
80 | }
81 | ),
82 |
83 | removeItem: thunk(async (actions, id, { getState, injections: { api } }) => {
84 | const { id: cartId } = getState()
85 |
86 | const payload = await api.delete(`carts/${cartId}/items/${id}`)
87 |
88 | actions.setCart(payload)
89 | }),
90 |
91 | addPromotion: thunk(
92 | async (actions, code, { getState, injections: { api } }) => {
93 | const { id: cartId } = getState()
94 |
95 | try {
96 | const payload = await api.post(`carts/${cartId}/items`, {
97 | type: 'promotion_item',
98 | code
99 | })
100 |
101 | actions.setCart(payload)
102 | } catch ({ statusCode }) {
103 | throw new Error('Code expired or invalid')
104 | }
105 | }
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/src/model/checkout.js:
--------------------------------------------------------------------------------
1 | import { thunk, action } from 'easy-peasy'
2 |
3 | import { changeRoute } from '../utils'
4 |
5 | export default {
6 | route: 'shipping',
7 | dirty: false,
8 | completed: false,
9 |
10 | goToShipping: changeRoute('shipping'),
11 | goToPayment: changeRoute('payment'),
12 |
13 | createOrder: thunk(
14 | async (
15 | _,
16 | {
17 | customerId,
18 | customer: initialCustomer,
19 | shipping_address,
20 | billing_address = shipping_address
21 | },
22 | { getStoreState, dispatch, injections: { api } }
23 | ) => {
24 | const {
25 | cart: { id: cartId }
26 | } = getStoreState()
27 |
28 | const customer = {
29 | name: `${billing_address.first_name} ${billing_address.last_name}`,
30 | ...initialCustomer
31 | }
32 |
33 | const createCustomer = customer && customer.password
34 |
35 |
36 | if (createCustomer) {
37 | const { data: newCustomer } = await api.post(`customers`, {
38 | type: 'customer',
39 | ...customer
40 | })
41 |
42 | customerId = newCustomer.id
43 |
44 | const { data: customerAuth } = await api.post(`customers/tokens`, {
45 | type: 'token',
46 | email: customer.email,
47 | password: customer.password
48 | })
49 |
50 | dispatch.user.setCustomerId(customerId)
51 | dispatch.user.setCustomerToken(customerAuth.token)
52 | }
53 |
54 | const { data } = await api.post(`carts/${cartId}/checkout`, {
55 | ...(createCustomer || customerId
56 | ? { customer: { id: customerId } }
57 | : { customer }),
58 | shipping_address,
59 | billing_address
60 | })
61 |
62 | return data
63 | }
64 | ),
65 |
66 | payForOrder: thunk(
67 | async (actions, { orderId, token }, { injections: { api } }) => {
68 | const { payment } = await api.post(`orders/${orderId}/payments`, {
69 | gateway: 'stripe',
70 | method: 'purchase',
71 | payment: token
72 | })
73 |
74 | actions.setCompleted()
75 |
76 | return payment
77 | }
78 | ),
79 |
80 | setDirty: action((state, dirty) => {
81 | state.dirty = dirty
82 | }),
83 |
84 | setCompleted: action((state, completed = true) => {
85 | state.dirty = false
86 | state.completed = completed
87 | })
88 | }
89 |
--------------------------------------------------------------------------------
/src/model/index.js:
--------------------------------------------------------------------------------
1 | import { thunk } from 'easy-peasy'
2 |
3 | import modal from './modal'
4 | import user from './user'
5 | import cart from './cart'
6 | import checkout from './checkout'
7 |
8 | export default {
9 | initialize: thunk(
10 | async (_, { cartId, customerId, customerToken }, { dispatch }) => {
11 | await dispatch.cart.getCart(cartId)
12 | await dispatch.cart.setCartId(cartId)
13 | await dispatch.user.setCustomerId(customerId)
14 | }
15 | ),
16 | modal,
17 | user,
18 | cart,
19 | checkout
20 | }
21 |
--------------------------------------------------------------------------------
/src/model/modal.js:
--------------------------------------------------------------------------------
1 | import { action, select, thunk } from 'easy-peasy'
2 |
3 | import { changeRoute } from '../utils'
4 |
5 | export default {
6 | route: 'cart',
7 | // route: 'billing',
8 | open: false,
9 | // open: true,
10 |
11 | checkingOut: select(({ route }) => ['shipping', 'billing'].includes(route)),
12 |
13 | goToCart: changeRoute('cart'),
14 | goToShipping: changeRoute('shipping'),
15 | goToBilling: changeRoute('billing'),
16 | goToConfirmation: changeRoute('confirmation'),
17 | goToOrders: changeRoute('orders'),
18 | goToLogin: changeRoute('login'),
19 |
20 | toggle: action(state => {
21 | state.open = !state.open
22 | }),
23 |
24 | openCart: action(state => {
25 | state.open = true
26 | state.route = 'cart'
27 | }),
28 |
29 | closeModal: thunk(async (actions, _, { getStoreState, getState }) => {
30 | const { checkingOut } = await getState()
31 | const {
32 | checkout: { completed }
33 | } = await getStoreState()
34 |
35 | if (!completed && checkingOut) return
36 |
37 | actions.close()
38 | }),
39 |
40 | close: action(state => {
41 | state.open = false
42 | }),
43 |
44 | continueShopping: action(state => {
45 | state.open = false
46 | state.route = 'cart'
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/src/model/user.js:
--------------------------------------------------------------------------------
1 | import { action, thunk, select } from 'easy-peasy'
2 |
3 | export default {
4 | id: null,
5 | token: null,
6 | orders: [],
7 |
8 | loggedIn: select(({ id, token }) => id && token),
9 |
10 | setCustomerId: action((state, customerId) => {
11 | state.id = customerId
12 | }),
13 |
14 | setCustomerToken: action((state, customerToken) => {
15 | state.token = customerToken
16 | }),
17 |
18 | // getUser: thunk(async (actions, {customerId, customerToken}, { injections: { api } }) => {
19 | // const payload = await api.get(`carts/${id}/items`)
20 |
21 | // actions.setCart(payload)
22 | // }),
23 |
24 | getOrders: thunk(
25 | async (actions, _, { getState, dispatch, injections: { api } }) => {
26 | const { token } = await getState()
27 | const { data } = await api.get(`orders?include=items`, {
28 | 'X-Moltin-Customer-Token': token
29 | })
30 |
31 | actions.setOrders(data)
32 | return data
33 | }
34 | ),
35 |
36 | login: thunk(
37 | async (_, { email, password }, { dispatch, injections: { api } }) => {
38 | try {
39 | const { data } = await api.post(`customers/tokens`, {
40 | type: 'token',
41 | email,
42 | password
43 | })
44 |
45 | dispatch.user.setCustomerId(data.customer_id)
46 | dispatch.user.setCustomerToken(data.token)
47 |
48 | return data
49 | } catch (error) {
50 | console.log(error.message)
51 | throw new Error('Incorrect email or password. Try again.')
52 | }
53 | }
54 | ),
55 |
56 | setOrders: action((state, orders) => {
57 | state.orders = orders
58 | })
59 | }
60 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | const theme = {
2 | primary: '#177EE6',
3 | dark: '#273142',
4 | text: '#333',
5 | border: '#C5CCD6',
6 | divider: '#D8DFEB',
7 | error: '#E62F17',
8 | success: '#5FC8AA',
9 | white: '#fff',
10 | placeholder: '#58697F',
11 | cursor: '#E9EBF0',
12 |
13 | textSmall: '0.875rem',
14 | textBase: '0.9375rem',
15 | textLarge: '1.125rem',
16 | textExtraLarge: '1.25rem'
17 | }
18 |
19 | export default theme
20 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { action } from 'easy-peasy'
2 |
3 | export const normalizeAttributes = target =>
4 | [...target.attributes].reduce((attributes, a) => {
5 | const startsWith = 'data-moltin-'
6 |
7 | if (!a.name.startsWith(startsWith)) return false
8 |
9 | return {
10 | ...attributes,
11 | [a.name.replace(startsWith, '')]: a.value
12 | }
13 | }, {})
14 |
15 | export const changeRoute = route =>
16 | action(state => {
17 | state.route = route
18 | state.open = true
19 | })
20 |
21 | export const pluralize = (count, noun, suffix = 's') =>
22 | `${count} ${noun}${count !== 1 ? suffix : ''}`
23 |
24 | export const validateEmail = email => {
25 | var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
26 |
27 | return re.test(String(email).toLowerCase())
28 | }
29 |
--------------------------------------------------------------------------------
/src/validation/auth.js:
--------------------------------------------------------------------------------
1 | export const loginValidation = values => {
2 | const errors = {}
3 |
4 | if (!values.email) {
5 | errors.email = 'Required'
6 | }
7 |
8 | if (!values.password) {
9 | errors.password = 'Required'
10 | }
11 |
12 | return errors
13 | }
14 |
--------------------------------------------------------------------------------
/src/validation/checkout.js:
--------------------------------------------------------------------------------
1 | import { validateEmail } from '../utils'
2 |
3 | export const shippingValidation = values => {
4 | const errors = {}
5 |
6 | if (!values.shipping_address || !values.shipping_address.first_name) {
7 | if (!errors.shipping_address) {
8 | errors.shipping_address = {}
9 | }
10 |
11 | errors.shipping_address.first_name = 'Required'
12 | }
13 |
14 | if (!values.shipping_address || !values.shipping_address.last_name) {
15 | if (!errors.shipping_address) {
16 | errors.shipping_address = {}
17 | }
18 |
19 | errors.shipping_address.last_name = 'Required'
20 | }
21 |
22 | if (!values.shipping_address || !values.shipping_address.line_1) {
23 | if (!errors.shipping_address) {
24 | errors.shipping_address = {}
25 | }
26 |
27 | errors.shipping_address.line_1 = 'Required'
28 | }
29 |
30 | if (!values.shipping_address || !values.shipping_address.city) {
31 | if (!errors.shipping_address) {
32 | errors.shipping_address = {}
33 | }
34 |
35 | errors.shipping_address.city = 'Required'
36 | }
37 |
38 | if (!values.shipping_address || !values.shipping_address.county) {
39 | if (!errors.shipping_address) {
40 | errors.shipping_address = {}
41 | }
42 |
43 | errors.shipping_address.county = 'Required'
44 | }
45 |
46 | if (!values.shipping_address || !values.shipping_address.postcode) {
47 | if (!errors.shipping_address) {
48 | errors.shipping_address = {}
49 | }
50 |
51 | errors.shipping_address.postcode = 'Required'
52 | }
53 |
54 | if (!values.shipping_address || !values.shipping_address.country) {
55 | if (!errors.shipping_address) {
56 | errors.shipping_address = {}
57 | }
58 |
59 | errors.shipping_address.country = 'Required'
60 | }
61 |
62 | return errors
63 | }
64 |
65 | export const billingValidation = values => {
66 | const errors = {}
67 |
68 | if (!values.billingIsShipping) {
69 | if (!values.billing_address || !values.billing_address.first_name) {
70 | if (!errors.billing_address) {
71 | errors.billing_address = {}
72 | }
73 |
74 | errors.billing_address.first_name = 'Required'
75 | }
76 |
77 | if (!values.billing_address || !values.billing_address.last_name) {
78 | if (!errors.billing_address) {
79 | errors.billing_address = {}
80 | }
81 |
82 | errors.billing_address.last_name = 'Required'
83 | }
84 |
85 | if (!values.billing_address || !values.billing_address.line_1) {
86 | if (!errors.billing_address) {
87 | errors.billing_address = {}
88 | }
89 |
90 | errors.billing_address.line_1 = 'Required'
91 | }
92 |
93 | if (!values.billing_address || !values.billing_address.city) {
94 | if (!errors.billing_address) {
95 | errors.billing_address = {}
96 | }
97 |
98 | errors.billing_address.city = 'Required'
99 | }
100 |
101 | if (!values.billing_address || !values.billing_address.county) {
102 | if (!errors.billing_address) {
103 | errors.billing_address = {}
104 | }
105 |
106 | errors.billing_address.county = 'Required'
107 | }
108 |
109 | if (!values.billing_address || !values.billing_address.postcode) {
110 | if (!errors.billing_address) {
111 | errors.billing_address = {}
112 | }
113 |
114 | errors.billing_address.postcode = 'Required'
115 | }
116 |
117 | if (!values.billing_address || !values.billing_address.country) {
118 | if (!errors.billing_address) {
119 | errors.billing_address = {}
120 | }
121 |
122 | errors.billing_address.country = 'Required'
123 | }
124 | }
125 |
126 | // if (!values.customer || !values.customer.name) {
127 | // if (!errors.customer) {
128 | // errors.customer = {}
129 | // }
130 |
131 | // errors.customer.name = 'Required'
132 | // }
133 |
134 | if (!values.customer || !values.customer.email) {
135 | if (!errors.customer) {
136 | errors.customer = {}
137 | }
138 |
139 | errors.customer.email = 'Required'
140 | }
141 |
142 | if (
143 | values.customer &&
144 | values.customer.email &&
145 | !validateEmail(values.customer.email)
146 | ) {
147 | if (!errors.customer) {
148 | errors.customer = {}
149 | }
150 |
151 | errors.customer.email = 'Invalid email'
152 | }
153 |
154 | if (
155 | values.createCustomer &&
156 | (!values.customer || !values.customer.password)
157 | ) {
158 | if (!errors.customer) {
159 | errors.customer = {}
160 | }
161 |
162 | errors.customer.password = 'Required'
163 | }
164 |
165 | if (!values.stripe || !values.stripe.complete) {
166 | if (!errors.stripe) {
167 | errors.stripe = {}
168 | }
169 |
170 | errors.stripe.complete = 'Required'
171 | }
172 |
173 | return errors
174 | }
175 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { join, resolve } = require('path')
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
3 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
4 | const JavaScriptObfuscator = require('webpack-obfuscator')
5 | const TerserPlugin = require('terser-webpack-plugin')
6 |
7 | const { NODE_ENV } = process.env
8 |
9 | const devMode = NODE_ENV !== 'production'
10 |
11 | module.exports = {
12 | mode: devMode ? 'development' : 'production',
13 | devtool: 'inline-source-map',
14 | devServer: {
15 | contentBase: join(__dirname, 'example'),
16 | compress: true,
17 | port: 3000,
18 | open: true,
19 | overlay: true,
20 | stats: 'minimal'
21 | },
22 | module: {
23 | rules: [
24 | {
25 | test: /\.(js|jsx)$/,
26 | exclude: /node_modules/,
27 | use: {
28 | loader: 'babel-loader'
29 | }
30 | },
31 | {
32 | test: /\.css$/,
33 | use: [MiniCssExtractPlugin.loader, 'css-loader']
34 | }
35 | ]
36 | },
37 | resolve: {
38 | extensions: ['*', '.js', '.jsx']
39 | },
40 | output: {
41 | path: resolve(__dirname, 'dist'),
42 | filename: 'index.js',
43 | library: 'MoltinShopkit',
44 | libraryExport: 'default',
45 | libraryTarget: 'window'
46 | },
47 | optimization: {
48 | minimizer: [
49 | new OptimizeCSSAssetsPlugin({}),
50 | new TerserPlugin({
51 | cache: true,
52 | parallel: true,
53 | terserOptions: {
54 | safari10: true,
55 | ie8: true,
56 | ecma: 5
57 | }
58 | })
59 | ],
60 | splitChunks: {
61 | cacheGroups: {
62 | styles: {
63 | name: 'styles',
64 | test: /\.css$/,
65 | chunks: 'all',
66 | enforce: true
67 | }
68 | }
69 | }
70 | },
71 | plugins: [
72 | devMode ? null : new JavaScriptObfuscator(),
73 | new MiniCssExtractPlugin({
74 | filename: devMode ? '[name].css' : '[name].[hash].css',
75 | chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
76 | })
77 | ].filter(i => i)
78 | }
79 |
--------------------------------------------------------------------------------