diff --git a/package.json b/package.json
index b1d825397a..a10503fbd2 100644
--- a/package.json
+++ b/package.json
@@ -129,6 +129,7 @@
],
"coverageDirectory": "reports/coverage",
"reporters": [
+ "default",
"jest-junit"
],
"roots": [
diff --git a/src/components/AccountDropdown/AccountDropdown.js b/src/components/AccountDropdown/AccountDropdown.js
index 2045e72513..cfce399ad6 100644
--- a/src/components/AccountDropdown/AccountDropdown.js
+++ b/src/components/AccountDropdown/AccountDropdown.js
@@ -14,7 +14,7 @@ const styles = (theme) => ({
width: 320,
padding: theme.spacing.unit * 2
},
- authContent: {
+ marginBottom: {
marginBottom: theme.spacing.unit * 2
}
});
@@ -73,6 +73,11 @@ class AccountDropdown extends Component {
{authStore.isAuthenticated ?
+
+
+
diff --git a/src/components/ProfileAddressBook/ProfileAddressBook.js b/src/components/ProfileAddressBook/ProfileAddressBook.js
new file mode 100644
index 0000000000..f8db7ed025
--- /dev/null
+++ b/src/components/ProfileAddressBook/ProfileAddressBook.js
@@ -0,0 +1,75 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { inject, observer } from "mobx-react";
+import Grid from "@material-ui/core/Grid";
+import { withStyles } from "@material-ui/core/styles";
+import AddressBook from "@reactioncommerce/components/AddressBook/v1";
+import withAddressBook from "containers/address/withAddressBook";
+import relayConnectionToArray from "lib/utils/relayConnectionToArray";
+import ErrorPage from "../../pages/_error";
+
+const styles = (theme) => ({
+ accountProfileInfoContainer: {
+ marginBottom: theme.spacing.unit * 4
+ }
+});
+
+@withStyles(styles)
+@withAddressBook
+@inject("authStore")
+@inject("uiStore")
+@observer
+class ProfileAddressBook extends Component {
+ static propTypes = {
+ authStore: PropTypes.shape({
+ account: PropTypes.object.isRequired
+ }),
+ classes: PropTypes.object,
+ onAddressAdded: PropTypes.func.isRequired,
+ onAddressDeleted: PropTypes.func.isRequired,
+ onAddressEdited: PropTypes.func.isRequired,
+ shop: PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string
+ })
+ };
+
+ renderAddressBook() {
+ const {
+ authStore: { account: { addressBook } },
+ onAddressAdded,
+ onAddressEdited,
+ onAddressDeleted
+ } = this.props;
+ // Use relayConnectionToArray to remove edges / nodes levels from addressBook object
+ const addresses = (addressBook && relayConnectionToArray(addressBook)) || [];
+
+ // Create data object to pass to AddressBook component
+ const accountAddressBook = {
+ addressBook: addresses
+ };
+
+ return (
+
+ );
+ }
+
+ render() {
+ const { authStore: { account }, shop } = this.props;
+
+ if (account && !account._id) return ;
+
+ return (
+
+ {this.renderAddressBook()}
+
+ );
+ }
+}
+
+export default ProfileAddressBook;
diff --git a/src/components/ProfileAddressBook/index.js b/src/components/ProfileAddressBook/index.js
new file mode 100644
index 0000000000..db6aa58a66
--- /dev/null
+++ b/src/components/ProfileAddressBook/index.js
@@ -0,0 +1 @@
+export { default } from "./ProfileAddressBook";
diff --git a/src/containers/account/viewer.gql b/src/containers/account/viewer.gql
index 857ba0cdc1..a3773fce0c 100644
--- a/src/containers/account/viewer.gql
+++ b/src/containers/account/viewer.gql
@@ -1,6 +1,25 @@
query viewer {
viewer {
_id
+ addressBook {
+ edges {
+ node {
+ _id
+ address1
+ address2
+ city
+ company
+ country
+ fullName
+ isBillingDefault
+ isCommercial
+ isShippingDefault
+ phone
+ postal
+ region
+ }
+ }
+ }
emailRecords {
address
}
diff --git a/src/containers/address/fragments.gql b/src/containers/address/fragments.gql
new file mode 100644
index 0000000000..3bb4fcd978
--- /dev/null
+++ b/src/containers/address/fragments.gql
@@ -0,0 +1,20 @@
+fragment AddressCommon on Address {
+ address1
+ address2
+ city
+ postal
+ country
+ region
+}
+
+fragment FullAddressCommon on Address {
+ ...AddressCommon
+ _id
+ fullName
+ company
+ isBillingDefault
+ isCommercial
+ isShippingDefault
+ metafields
+ phone
+}
diff --git a/src/containers/address/mutations.gql b/src/containers/address/mutations.gql
new file mode 100644
index 0000000000..38aaf7f5bd
--- /dev/null
+++ b/src/containers/address/mutations.gql
@@ -0,0 +1,59 @@
+mutation addAccountAddressBookEntry($input: AddAccountAddressBookEntryInput!) {
+ addAccountAddressBookEntry(input: $input) {
+ address {
+ _id
+ address1
+ address2
+ city
+ company
+ country
+ fullName
+ isBillingDefault
+ isCommercial
+ isShippingDefault
+ phone
+ postal
+ region
+ }
+ }
+}
+
+mutation updateAccountAddressBookEntry($input: UpdateAccountAddressBookEntryInput!) {
+ updateAccountAddressBookEntry(input: $input) {
+ address {
+ _id
+ address1
+ address2
+ city
+ company
+ country
+ fullName
+ isBillingDefault
+ isCommercial
+ isShippingDefault
+ phone
+ postal
+ region
+ }
+ }
+}
+
+mutation removeAccountAddressBookEntry($input: RemoveAccountAddressBookEntryInput!) {
+ removeAccountAddressBookEntry(input: $input) {
+ address {
+ _id
+ address1
+ address2
+ city
+ company
+ country
+ fullName
+ isBillingDefault
+ isCommercial
+ isShippingDefault
+ phone
+ postal
+ region
+ }
+ }
+}
diff --git a/src/containers/address/withAddressBook.js b/src/containers/address/withAddressBook.js
new file mode 100644
index 0000000000..7928c33fe9
--- /dev/null
+++ b/src/containers/address/withAddressBook.js
@@ -0,0 +1,160 @@
+import React, { Component, Fragment } from "react";
+import PropTypes from "prop-types";
+import hoistNonReactStatic from "hoist-non-react-statics";
+import { withApollo } from "react-apollo";
+import { inject, observer } from "mobx-react";
+import relayConnectionToArray from "lib/utils/relayConnectionToArray";
+import viewerQuery from "../account/viewer.gql";
+import {
+ addAccountAddressBookEntry,
+ updateAccountAddressBookEntry,
+ removeAccountAddressBookEntry
+} from "./mutations.gql";
+
+export default function withAddressBook(Comp) {
+ @withApollo
+ @inject("authStore")
+ @observer
+ class WithAddressBook extends Component {
+ static propTypes = {
+ authStore: PropTypes.shape({
+ account: PropTypes.object.isRequired
+ }),
+ client: PropTypes.shape({
+ query: PropTypes.func.isRequired
+ })
+ };
+
+ state = {};
+
+ get accountId() {
+ const { authStore } = this.props;
+ return authStore && authStore.account && authStore.account._id;
+ }
+
+ get accountAddressBook() {
+ const { authStore: { account: { addressBook } } } = this.props;
+ return (addressBook && relayConnectionToArray(addressBook)) || [];
+ }
+
+ /**
+ * @name handleAddAccountAddressBookEntry
+ * @summary Adds an address to current user's address book
+ * @param {Object} address Address to add
+ * @return {Undefined} undefined
+ */
+ handleAddAccountAddressBookEntry = (address) => {
+ const { client: apolloClient } = this.props;
+
+ // TEMP delete `addressName` prop until API supports it.
+ delete address.addressName;
+
+ apolloClient.mutate({
+ mutation: addAccountAddressBookEntry,
+ variables: {
+ input: {
+ address,
+ accountId: this.accountId
+ }
+ },
+ update: (cache, { data: mutationData }) => {
+ if (mutationData && mutationData.addAccountAddressBookEntry) {
+ const { address: newAddressEntry } = mutationData.addAccountAddressBookEntry;
+ if (newAddressEntry) {
+ const cacheData = cache.readQuery({ query: viewerQuery });
+ if (!cacheData.viewer.addressBook) {
+ cacheData.viewer.addressBook = { edges: [] };
+ }
+ cacheData.viewer.addressBook.edges.push({
+ __typename: "AddressEdge",
+ node: newAddressEntry
+ });
+ // Update Apollo cache
+ cache.writeQuery({
+ query: viewerQuery,
+ data: cacheData
+ });
+ }
+ }
+ }
+ });
+ };
+
+ /**
+ * @name handleEditAccountAddressBookEntry
+ * @summary Updates an address in current user's address book
+ * @param {String} addressId _id of address to update
+ * @param {Object} updates Field updates
+ * @return {Undefined} undefined
+ */
+ handleEditAccountAddressBookEntry = (addressId, updates) => {
+ const { client: apolloClient } = this.props;
+ apolloClient.mutate({
+ mutation: updateAccountAddressBookEntry,
+ variables: {
+ input: {
+ addressId,
+ accountId: this.accountId,
+ updates
+ }
+ }
+ });
+ };
+
+ /**
+ * @name handleRemoveAccountAddressBookEntry
+ * @summary Asks user to confirm, then deletes address from current user's address book
+ * @param {String} addressId _id of address to delete
+ * @return {Undefined} undefined
+ */
+ handleRemoveAccountAddressBookEntry = (addressId) => {
+ const { client: apolloClient } = this.props;
+
+ if (!confirm("Delete this address?")) {
+ return;
+ }
+
+ apolloClient.mutate({
+ mutation: removeAccountAddressBookEntry,
+ variables: {
+ input: {
+ addressId,
+ accountId: this.accountId
+ }
+ },
+ update: (cache, { data: mutationData }) => {
+ if (mutationData && mutationData.removeAccountAddressBookEntry) {
+ const { address: removedAddressEntry } = mutationData.removeAccountAddressBookEntry;
+ if (removedAddressEntry) {
+ const cacheData = cache.readQuery({ query: viewerQuery });
+ const removedIndex = cacheData.viewer.addressBook.edges.findIndex((edge) => edge.node._id === addressId);
+ cacheData.viewer.addressBook.edges.splice(removedIndex, 1);
+ // Update Apollo cache
+ cache.writeQuery({
+ query: viewerQuery,
+ data: cacheData
+ });
+ }
+ }
+ }
+ });
+ };
+
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ hoistNonReactStatic(WithAddressBook, Comp);
+
+ return WithAddressBook;
+}
diff --git a/src/custom/componentsContext.js b/src/custom/componentsContext.js
index b407a92844..c5d89af94c 100644
--- a/src/custom/componentsContext.js
+++ b/src/custom/componentsContext.js
@@ -16,11 +16,16 @@ import iconAmericanExpress from "@reactioncommerce/components/svg/iconAmericanEx
import iconClear from "@reactioncommerce/components/svg/iconClear";
import iconDiscover from "@reactioncommerce/components/svg/iconDiscover";
import iconError from "@reactioncommerce/components/svg/iconError";
+import iconExpand from "@reactioncommerce/components/svg/iconExpand";
import iconLock from "@reactioncommerce/components/svg/iconLock";
+import iconPlus from "@reactioncommerce/components/svg/iconPlus";
import iconMastercard from "@reactioncommerce/components/svg/iconMastercard";
import iconValid from "@reactioncommerce/components/svg/iconValid";
import iconVisa from "@reactioncommerce/components/svg/iconVisa";
import spinner from "@reactioncommerce/components/svg/spinner";
+import Accordion from "@reactioncommerce/components/Accordion/v1";
+import AccordionFormList from "@reactioncommerce/components/AccordionFormList/v1";
+import AddressBook from "@reactioncommerce/components/AddressBook/v1";
import Address from "@reactioncommerce/components/Address/v1";
import AddressCapture from "@reactioncommerce/components/AddressCapture/v1";
import AddressChoice from "@reactioncommerce/components/AddressChoice/v1";
@@ -40,6 +45,7 @@ import CheckoutActionComplete from "@reactioncommerce/components/CheckoutActionC
import CheckoutActionIncomplete from "@reactioncommerce/components/CheckoutActionIncomplete/v1";
import ErrorsBlock from "@reactioncommerce/components/ErrorsBlock/v1";
import Field from "@reactioncommerce/components/Field/v1";
+import InPageMenuItem from "@reactioncommerce/components/InPageMenuItem/v1";
import InlineAlert from "@reactioncommerce/components/InlineAlert/v1";
import InventoryStatus from "@reactioncommerce/components/InventoryStatus/v1";
import Link from "components/Link";
@@ -62,6 +68,9 @@ import withLocales from "../lib/utils/withLocales";
const AddressFormWithLocales = withLocales(AddressForm);
export default {
+ Accordion,
+ AccordionFormList,
+ AddressBook,
Address,
AddressCapture,
AddressChoice,
@@ -88,10 +97,13 @@ export default {
iconClear,
iconDiscover,
iconError,
+ iconExpand,
iconLock,
iconMastercard,
+ iconPlus,
iconValid,
iconVisa,
+ InPageMenuItem,
MiniCartSummary,
PhoneNumberInput,
Price,
diff --git a/src/custom/routes.js b/src/custom/routes.js
index 7754fd6d8e..5b5114ee94 100644
--- a/src/custom/routes.js
+++ b/src/custom/routes.js
@@ -14,7 +14,10 @@ function defineRoutes(routes) {
.add("shopProduct", "/shop/:shopSlug/product/:slugOrId", "product")
.add("product", "/product/:slugOrId/:variantId?", "product")
.add("shop", "/shop/:shopId/:tag", "productGrid")
- .add("tag", "/tag/:slug", "tag");
+ .add("tag", "/tag/:slug", "tag")
+ .add("profileAddressBook", "/profile/address", "profile")
+ .add("profileOrders", "/profile/orders", "profile")
+ .add("profilePaymentMethods", "/profile/payments", "profile");
}
module.exports = defineRoutes;
diff --git a/src/pages/profile.js b/src/pages/profile.js
new file mode 100644
index 0000000000..5b05cf30b3
--- /dev/null
+++ b/src/pages/profile.js
@@ -0,0 +1,128 @@
+import React, { Component, Fragment } from "react";
+import PropTypes from "prop-types";
+import { inject, observer } from "mobx-react";
+import Helmet from "react-helmet";
+import Grid from "@material-ui/core/Grid";
+import { withStyles } from "@material-ui/core/styles";
+import AccountProfileInfo from "@reactioncommerce/components/AccountProfileInfo/v1";
+import InPageMenu from "@reactioncommerce/components/InPageMenu/v1";
+import ProfileAddressBook from "components/ProfileAddressBook";
+import withAddressBook from "containers/address/withAddressBook";
+import ErrorPage from "./_error";
+
+const styles = (theme) => ({
+ accountProfileInfoContainer: {
+ marginBottom: theme.spacing.unit * 4
+ }
+});
+
+@withStyles(styles)
+@withAddressBook
+@inject("authStore")
+@inject("uiStore")
+@observer
+class Profile extends Component {
+ static propTypes = {
+ authStore: PropTypes.shape({
+ account: PropTypes.object.isRequired
+ }),
+ classes: PropTypes.object,
+ onAddressAdded: PropTypes.func.isRequired,
+ onAddressDeleted: PropTypes.func.isRequired,
+ onAddressEdited: PropTypes.func.isRequired,
+ shop: PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string
+ })
+ };
+
+ renderMainContent() {
+ const { router: { asPath }, shop } = this.props;
+
+ if (asPath === "/profile/address") {
+ return ;
+ }
+
+ if (asPath === "/profile/orders") {
+ return "Orders placeholder";
+ }
+
+ if (asPath === "/profile/payments") {
+ return "Payment Methods placeholder";
+ }
+
+ return ;
+ }
+
+ renderAccountProfileInfo() {
+ const { authStore: { account }, classes } = this.props;
+
+ return (
+
+ );
+ }
+
+ renderNavigation() {
+ const { classes, router: { asPath } } = this.props;
+
+ const menuItems = [
+ {
+ href: "/profile/address",
+ route: "/profile/address",
+ label: "Address Book",
+ isSelected: asPath === "/profile/address"
+ }
+ // {
+ // href: "/profile/orders",
+ // route: "/profile/orders",
+ // label: "Orders",
+ // isSelected: asPath === "/profile/orders"
+ // },
+ // {
+ // href: "/profile/payments",
+ // route: "/profile/payments",
+ // label: "Payment Methods",
+ // isSelected: asPath === "/profile/payments"
+ // }
+ ];
+
+ return (
+
+
+
+ );
+ }
+
+ render() {
+ const { authStore: { account }, shop } = this.props;
+
+ // If there is no logged in user, return Not Found page
+ if (account && !account._id) return ;
+
+ return (
+
+
+
+
+ {/* MUI grid doesn't have an offset. Use blank grid item instead. */}
+
+ {this.renderAccountProfileInfo()}
+ {this.renderNavigation()}
+
+
+ {this.renderMainContent()}
+
+ {/* MUI grid doesn't have an offset. Use blank grid item instead. */}
+
+
+
+ );
+ }
+}
+
+export default Profile;