├── .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 | [![Stable Branch](https://img.shields.io/badge/stable%20branch-master-blue.svg)](https://github.com/moltin/shopkit) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/moltin/shopkit/issues) 8 | [![follow on Twitter](https://img.shields.io/twitter/follow/elasticpath?style=social&logo=twitter)](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 | 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 ? {name} : } 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 | 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 |
110 | {({ handleSubmit, submitting, invalid, values, form, dirty }) => { 111 | if ( 112 | !values.createCustomer && 113 | values.customer && 114 | values.customer.password 115 | ) { 116 | delete values.customer.password 117 | } 118 | 119 | if (values.billingIsShipping) { 120 | delete values.billing_address 121 | } 122 | 123 | const onStripeChange = e => form.change('stripe', e) 124 | 125 | setDirty(dirty) 126 | 127 | return ( 128 | 129 | {route === 'shipping' ? ( 130 |
131 | 132 | Shipping information 133 | 134 | 135 |
136 | 141 |
142 | 143 |
144 | 149 | Continue to billing information 150 | 151 |
152 |
153 | ) : ( 154 |
155 | 156 | Billing information 157 | 158 | 159 |
160 | 164 | 165 | {!values.billingIsShipping && ( 166 | 171 | )} 172 |
173 | 174 | 175 | 181 | 182 | 183 | 184 | 206 | {values.stripe && values.stripe.error && ( 207 | {values.stripe.error.message} 208 | )} 209 | 210 | 211 | 212 | {/* 213 | 217 | 218 | {values.createCustomer && ( 219 | 224 | )} 225 | */} 226 | 227 |
228 | 233 | Pay {subTotal} 234 | 235 |
236 | 237 | 242 |
243 | )} 244 |
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 | 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 |
32 | {({ handleSubmit, submitting, invalid, pristine }) => { 33 | pristine && setError(null) 34 | 35 | return ( 36 | 37 | 38 | 44 | 45 | 51 | Login 52 | 53 |
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 |