@riosst100/pwa-marketplace 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/package.json +16 -0
  2. package/src/Utilities/graphQL.js +76 -0
  3. package/src/componentOverrideMapping.js +11 -0
  4. package/src/components/Header/websiteSwitcher.js +109 -0
  5. package/src/components/Header/websiteSwitcher.module.css +111 -0
  6. package/src/components/Header/websiteSwitcher.shimmer.js +6 -0
  7. package/src/components/Header/websiteSwitcherItem.js +47 -0
  8. package/src/components/Header/websiteSwitcherItem.module.css +20 -0
  9. package/src/index.js +7 -0
  10. package/src/intercept.js +50 -0
  11. package/src/moduleOverrideWebpackPlugin.js +71 -0
  12. package/src/overwrites/peregrine/lib/talons/Adapter/useAdapter.js +208 -0
  13. package/src/overwrites/peregrine/lib/talons/Header/storeSwitcher.gql.js +45 -0
  14. package/src/overwrites/peregrine/lib/talons/Header/useStoreSwitcher.js +204 -0
  15. package/src/overwrites/pwa-buildpack/lib/queries/getAvailableStoresConfigData.graphql +11 -0
  16. package/src/overwrites/venia-ui/lib/components/Adapter/adapter.js +110 -0
  17. package/src/overwrites/venia-ui/lib/components/Header/header.js +116 -0
  18. package/src/overwrites/venia-ui/lib/components/Header/storeSwitcher.js +118 -0
  19. package/src/overwrites/venia-ui/lib/components/Header/switcherItem.js +47 -0
  20. package/src/overwrites/venia-ui/lib/components/StoreCodeRoute/storeCodeRoute.js +75 -0
  21. package/src/queries/getAvailableWebsitesConfigData.graphql +14 -0
  22. package/src/queries/index.js +30 -0
  23. package/src/talons/Header/useWebsiteSwitcher.js +219 -0
  24. package/src/talons/Header/websiteSwitcher.gql.js +45 -0
  25. package/src/talons/WebsiteByIp/getWebsiteByIp.gql.js +14 -0
  26. package/src/talons/WebsiteByIp/useWebsiteByIp.js +33 -0
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { shape, string } from 'prop-types';
3
+
4
+ import { useStoreSwitcher } from '@magento/peregrine/lib/talons/Header/useStoreSwitcher';
5
+ import { availableRoutes } from '@magento/venia-ui/lib/components/Routes/routes';
6
+
7
+ import { useStyle } from '@magento/venia-ui/lib/classify';
8
+ import defaultClasses from '@magento/venia-ui/lib/components/Header/storeSwitcher.module.css';
9
+ import SwitcherItem from '@magento/venia-ui/lib/components/Header/switcherItem';
10
+ import Shimmer from '@magento/venia-ui/lib/components/Header/storeSwitcher.shimmer';
11
+
12
+ const StoreSwitcher = props => {
13
+ const {
14
+ availableStores,
15
+ currentGroupName,
16
+ currentStoreName,
17
+ handleSwitchStore,
18
+ storeGroups,
19
+ storeMenuRef,
20
+ storeMenuTriggerRef,
21
+ storeMenuIsOpen,
22
+ handleTriggerClick
23
+ } = useStoreSwitcher({ availableRoutes });
24
+
25
+ const classes = useStyle(defaultClasses, props.classes);
26
+ const menuClassName = storeMenuIsOpen ? classes.menu_open : classes.menu;
27
+
28
+ if (!availableStores) return <Shimmer />;
29
+
30
+ const groups = [];
31
+ const hasOnlyOneGroup = storeGroups.size === 1;
32
+
33
+ storeGroups.forEach((group, key) => {
34
+ const stores = [];
35
+ group.forEach(({ storeGroupName, storeName, isCurrent, storeCode, websiteCode }) => {
36
+ let label;
37
+ if (hasOnlyOneGroup) {
38
+ label = `${storeName}`;
39
+ } else {
40
+ label = `${storeGroupName} - ${storeName}`;
41
+ }
42
+
43
+ stores.push(
44
+ <li
45
+ aria-selected={currentStoreName}
46
+ role="option"
47
+ key={storeCode}
48
+ className={classes.menuItem}
49
+ data-cy="StoreSwitcher-view"
50
+ >
51
+ <SwitcherItem
52
+ active={isCurrent}
53
+ onClick={handleSwitchStore}
54
+ option={storeCode}
55
+ option2={websiteCode}
56
+ >
57
+ {label}
58
+ </SwitcherItem>
59
+ </li>
60
+ );
61
+ });
62
+
63
+ groups.push(
64
+ <ul
65
+ role="listbox"
66
+ className={classes.groupList}
67
+ key={key}
68
+ data-cy="StoreSwitcher-group"
69
+ >
70
+ {stores}
71
+ </ul>
72
+ );
73
+ });
74
+
75
+ let triggerLabel;
76
+ if (hasOnlyOneGroup) {
77
+ triggerLabel = `${currentStoreName}`;
78
+ } else {
79
+ triggerLabel = `${currentGroupName} - ${currentStoreName}`;
80
+ }
81
+
82
+ return (
83
+ <div className={classes.root} data-cy="StoreSwitcher-root">
84
+ <button
85
+ data-cy="StoreSwitcher-triggerButton"
86
+ className={classes.trigger}
87
+ aria-label={currentStoreName || ''}
88
+ onClick={handleTriggerClick}
89
+ ref={storeMenuTriggerRef}
90
+ data-cy="StoreSwitcher-trigger"
91
+ aria-expanded={storeMenuIsOpen}
92
+ >
93
+ {triggerLabel}
94
+ </button>
95
+ <div
96
+ ref={storeMenuRef}
97
+ className={menuClassName}
98
+ data-cy="StoreSwitcher-menu"
99
+ >
100
+ <div className={classes.groups}>{groups}</div>
101
+ </div>
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export default StoreSwitcher;
107
+
108
+ StoreSwitcher.propTypes = {
109
+ classes: shape({
110
+ groupList: string,
111
+ groups: string,
112
+ menu: string,
113
+ menu_open: string,
114
+ menuItem: string,
115
+ root: string,
116
+ trigger: string
117
+ })
118
+ };
@@ -0,0 +1,47 @@
1
+ import React, { useCallback } from 'react';
2
+ import { Check } from 'react-feather';
3
+ import { bool, func, shape, string } from 'prop-types';
4
+
5
+ import { useStyle } from '@magento/venia-ui/lib/classify';
6
+ import Icon from '@magento/venia-ui/lib/components/Icon/icon';
7
+ import defaultClasses from '@magento/venia-ui/lib/components/Header/switcherItem.module.css';
8
+
9
+ const SwitcherItem = props => {
10
+ const { active, onClick, option, option2, children } = props;
11
+ const classes = useStyle(defaultClasses, props.classes);
12
+
13
+ const handleClick = useCallback(() => {
14
+ onClick(option, option2);
15
+ }, [option, option2, onClick]);
16
+
17
+ const activeIcon = active ? (
18
+ <Icon data-cy="SwitcherItem-activeIcon" size={20} src={Check} />
19
+ ) : null;
20
+
21
+ return (
22
+ <button
23
+ data-cy="SwitcherItem-button"
24
+ className={classes.root}
25
+ disabled={active}
26
+ onClick={handleClick}
27
+ >
28
+ <span className={classes.content}>
29
+ <span className={classes.text}>{children}</span>
30
+ {activeIcon}
31
+ </span>
32
+ </button>
33
+ );
34
+ };
35
+
36
+ SwitcherItem.propTypes = {
37
+ active: bool,
38
+ classes: shape({
39
+ content: string,
40
+ root: string,
41
+ text: string
42
+ }),
43
+ onClick: func,
44
+ option: string
45
+ };
46
+
47
+ export default SwitcherItem;
@@ -0,0 +1,75 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import { useHistory } from 'react-router-dom';
3
+ import { BrowserPersistence } from '@magento/peregrine/lib/util';
4
+
5
+ const storage = new BrowserPersistence();
6
+
7
+ /**
8
+ * This component checks for use of a store code in the url that is not the
9
+ * current base. If found, it updates the local storage values for code/currency
10
+ * and reloads the page so that they are used in the graphQL headers.
11
+ */
12
+ const StoreCodeRoute = () => {
13
+ const history = useHistory();
14
+
15
+ const websiteCodes = [];
16
+ const websiteStores = useMemo(() => ({}), []);
17
+ const storeCurrencies = useMemo(() => ({}), []);
18
+ const storeSecureBaseMediaUrl = useMemo(() => ({}), []);
19
+
20
+ AVAILABLE_WEBSITES.forEach(store => {
21
+ websiteCodes.push(store.website_code);
22
+ websiteStores[store.website_code] = store.store_code;
23
+ storeCurrencies[store.store_code] = store.default_display_currency_code;
24
+ storeSecureBaseMediaUrl[store.store_code] = store.secure_base_media_url;
25
+ });
26
+
27
+ // Sort by length (longest first) to avoid false hits ie "en" matching just
28
+ // the "/en" in "/en-us/home.html" when "en-us" is also in storeCodes.
29
+ websiteCodes.sort((a, b) => b.length - a.length);
30
+
31
+ // Find the store code in the url. This will always be the first path.
32
+ // ie `https://example.com/fr/foo/baz.html` => `fr`.
33
+ const { location } = globalThis;
34
+ const match = location && location.pathname.split("/")[1];
35
+ let websiteCodeInUrl = websiteCodes.find((str) => str === match);
36
+
37
+ // Determine what the current store code is using the configured basename.
38
+ const currentWebsiteCode = storage && storage.getItem('website_code') || process.env.WEBSITE_CODE;
39
+
40
+ // If we find a store code in the url that is not the current one, update
41
+ // the storage value and refresh so that we start using the new code.
42
+ useEffect(() => {
43
+ if (!websiteCodeInUrl) {
44
+ history.replace(location.pathname)
45
+ } else if (websiteCodeInUrl && websiteCodeInUrl !== currentWebsiteCode) {
46
+ let storeCodeInUrl = websiteStores[websiteCodeInUrl];
47
+
48
+ storage.setItem('store_view_code', storeCodeInUrl);
49
+ storage.setItem('website_code', websiteCodeInUrl);
50
+ storage.setItem(
51
+ 'store_view_currency',
52
+ storeCurrencies[storeCodeInUrl]
53
+ );
54
+ storage.setItem(
55
+ 'store_view_secure_base_media_url',
56
+ storeSecureBaseMediaUrl[storeCodeInUrl]
57
+ );
58
+
59
+ // We're required to reload the page as the basename doesn't
60
+ // change entirely without a full page reload.
61
+ history.go(0);
62
+ }
63
+ }, [
64
+ websiteCodeInUrl,
65
+ // currentWebsiteCode,
66
+ history
67
+ // storeCodeInUrl,
68
+ // storeCurrencies,
69
+ // storeSecureBaseMediaUrl
70
+ ]);
71
+
72
+ return null;
73
+ };
74
+
75
+ export default StoreCodeRoute;
@@ -0,0 +1,14 @@
1
+ query getAvailableWebsitesConfigData {
2
+ availableStoresByUserIp {
3
+ store_code
4
+ id
5
+ secure_base_media_url
6
+ store_name
7
+ default_display_currency_code
8
+ website_code
9
+ is_default_store
10
+ website_code
11
+ website_name
12
+ is_suggested
13
+ }
14
+ }
@@ -0,0 +1,30 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Normal `require` doesn't know what to do with .graphql files, so this helper function
6
+ * simply imports their contents as a string.
7
+ * @see https://github.com/apollographql/apollo-server/issues/1175#issuecomment-397257339.
8
+ *
9
+ * @param {String} filepath - A relative path to a .graphql file to read.
10
+ * @returns {String} - The contents of the file as a string.
11
+ */
12
+ const requireGraphQL = filePath => {
13
+ const absolutePath = path.resolve(__dirname, filePath);
14
+ return stripComments(fs.readFileSync(absolutePath, { encoding: 'utf8' }));
15
+ };
16
+
17
+ const singleLineCommentRegex = /(^#.*\n)/gm;
18
+ const stripComments = string => {
19
+ return string.replace(singleLineCommentRegex, '');
20
+ };
21
+
22
+ // Import all the build-time queries.
23
+ const getAvailableWebsitesConfigData = requireGraphQL(
24
+ './getAvailableWebsitesConfigData.graphql'
25
+ );
26
+
27
+ // Export the queries for use by the rest of buildpack.
28
+ module.exports = {
29
+ getAvailableWebsitesConfigData
30
+ };
@@ -0,0 +1,219 @@
1
+ import { useQuery } from '@apollo/client';
2
+ import { useCallback, useMemo } from 'react';
3
+ import { useLocation } from 'react-router-dom';
4
+ import { useDropdown } from '@magento/peregrine/lib/hooks/useDropdown';
5
+ import { useAwaitQuery } from '@magento/peregrine/lib/hooks/useAwaitQuery';
6
+ import { BrowserPersistence } from '@magento/peregrine/lib/util';
7
+ import mergeOperations from '@magento/peregrine/lib/util/shallowMerge';
8
+ import DEFAULT_OPERATIONS from './websiteSwitcher.gql';
9
+
10
+ const storage = new BrowserPersistence();
11
+
12
+ const mapAvailableOptions = (config, stores) => {
13
+ const { store_code: configCode } = config;
14
+
15
+ return stores.reduce((map, store) => {
16
+ const {
17
+ default_display_currency_code: currency,
18
+ locale,
19
+ secure_base_media_url,
20
+ store_code: storeCode,
21
+ store_group_code: storeGroupCode,
22
+ store_group_name: storeGroupName,
23
+ website_name: websiteName,
24
+ website_code: websiteCode,
25
+ store_sort_order: sortOrder
26
+ } = store;
27
+
28
+ const isCurrent = storeCode === configCode;
29
+ const option = {
30
+ currency,
31
+ isCurrent,
32
+ locale,
33
+ secure_base_media_url,
34
+ sortOrder,
35
+ storeCode,
36
+ storeGroupCode,
37
+ storeGroupName,
38
+ websiteName,
39
+ websiteCode
40
+ };
41
+
42
+ return map.set(storeCode, option);
43
+ }, new Map());
44
+ };
45
+
46
+ /**
47
+ * The useWebsiteSwitcher talon complements the WebsiteSwitcher component.
48
+ *
49
+ * @param {Array<Object>} [props.availableRoutes] - Hardcoded app routes.
50
+ * @param {Object} [props.operations] - GraphQL operations to be run by the hook.
51
+ *
52
+ * @returns {Map} talonProps.availableStores - Details about the available store views.
53
+ * @returns {String} talonProps.currentWebsiteName - Name of the current store view.
54
+ * @returns {Boolean} talonProps.storeMenuIsOpen - Whether the menu that this trigger toggles is open or not.
55
+ * @returns {Ref} talonProps.storeMenuRef - A React ref to the menu that this trigger toggles.
56
+ * @returns {Ref} talonProps.storeMenuTriggerRef - A React ref to the trigger element itself.
57
+ * @returns {Function} talonProps.handleTriggerClick - A function for handling when the trigger is clicked.
58
+ * @returns {Function} talonProps.handleSwitchWebsite - A function for handling when the menu item is clicked.
59
+ */
60
+
61
+ export const useWebsiteSwitcher = (props = {}) => {
62
+ const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
63
+ const { availableRoutes = [] } = props;
64
+ const internalRoutes = useMemo(() => {
65
+ return availableRoutes.map(path => {
66
+ if (path.exact) {
67
+ return path.pattern;
68
+ }
69
+ });
70
+ }, [availableRoutes]);
71
+
72
+ const {
73
+ getStoreConfigData,
74
+ getRouteData,
75
+ getAvailableWebsitesData
76
+ } = operations;
77
+ const { pathname, search: searchParams } = useLocation();
78
+ const {
79
+ elementRef: storeMenuRef,
80
+ expanded: storeMenuIsOpen,
81
+ setExpanded: setStoreMenuIsOpen,
82
+ triggerRef: storeMenuTriggerRef
83
+ } = useDropdown();
84
+
85
+ const { data: storeConfigData } = useQuery(getStoreConfigData, {
86
+ fetchPolicy: 'cache-and-network',
87
+ nextFetchPolicy: 'cache-first'
88
+ });
89
+
90
+ const fetchRouteData = useAwaitQuery(getRouteData);
91
+
92
+ const { data: availableStoresData } = useQuery(getAvailableWebsitesData, {
93
+ fetchPolicy: 'cache-and-network',
94
+ nextFetchPolicy: 'cache-first'
95
+ });
96
+
97
+ const currentWebsiteName = useMemo(() => {
98
+ if (storeConfigData) {
99
+ return storeConfigData.storeConfig.website_name;
100
+ }
101
+ }, [storeConfigData]);
102
+
103
+ const currentGroupName = useMemo(() => {
104
+ if (storeConfigData) {
105
+ return storeConfigData.storeConfig.store_group_name;
106
+ }
107
+ }, [storeConfigData]);
108
+
109
+ // availableStores => mapped options or empty map if undefined.
110
+ const availableStores = useMemo(() => {
111
+ console.log(availableStoresData)
112
+ return (
113
+ (storeConfigData &&
114
+ availableStoresData &&
115
+ mapAvailableOptions(
116
+ storeConfigData.storeConfig,
117
+ availableStoresData.availableStoresByUserIp
118
+ )) ||
119
+ new Map()
120
+ );
121
+ }, [storeConfigData, availableStoresData]);
122
+
123
+ // Create a map of sorted store views for each group.
124
+ const storeGroups = useMemo(() => {
125
+ const groups = new Map();
126
+
127
+ availableStores.forEach(store => {
128
+ const groupCode = store.storeGroupCode;
129
+ if (!groups.has(groupCode)) {
130
+ const groupViews = [store];
131
+ groups.set(groupCode, groupViews);
132
+ } else {
133
+ const groupViews = groups.get(groupCode);
134
+ // Insert store at configured position
135
+ groupViews.splice(store.sortOrder, 0, store);
136
+ }
137
+ });
138
+
139
+ return groups;
140
+ }, [availableStores]);
141
+
142
+ const getPathname = useCallback(
143
+ async storeCode => {
144
+ if (pathname === '' || pathname === '/') return '';
145
+ let newPath = '';
146
+ if (internalRoutes.includes(pathname)) {
147
+ newPath = pathname;
148
+ } else {
149
+ const { data: routeData } = await fetchRouteData({
150
+ fetchPolicy: 'no-cache',
151
+ variables: {
152
+ url: pathname
153
+ },
154
+ context: { headers: { store: storeCode } }
155
+ });
156
+ if (routeData.route) {
157
+ newPath = routeData.route.relative_url;
158
+ }
159
+ }
160
+ return newPath.startsWith('/') ? newPath.substr(1) : newPath;
161
+ },
162
+ [pathname, fetchRouteData, internalRoutes]
163
+ );
164
+
165
+ const handleSwitchWebsite = useCallback(
166
+ // Change store view code and currency to be used in Apollo link request headers
167
+ async (storeCode, websiteCode) => {
168
+ // Do nothing when store view is not present in available stores
169
+ if (!availableStores.has(storeCode)) return;
170
+
171
+ storage.setItem('store_view_code', storeCode);
172
+ storage.setItem(
173
+ 'store_view_currency',
174
+ availableStores.get(storeCode).currency
175
+ );
176
+ storage.setItem(
177
+ 'store_view_secure_base_media_url',
178
+ availableStores.get(storeCode).secure_base_media_url
179
+ );
180
+ const pathName = await getPathname(storeCode);
181
+
182
+ let accessBaseWebsite = null;
183
+ let newSearchParams = searchParams;
184
+ let newWebsiteCode = websiteCode;
185
+
186
+ if (newWebsiteCode == process.env.WEBSITE_CODE) {
187
+ newWebsiteCode = null;
188
+ newSearchParams = searchParams ? searchParams+'&access_base_website=1' : '?access_base_website=1';
189
+ accessBaseWebsite = '?access_base_website=1';
190
+ }
191
+
192
+ const newPath = pathName ? `/${pathName}${newSearchParams}` : (accessBaseWebsite ? `/${pathName}${accessBaseWebsite}` : '');
193
+
194
+ if (newWebsiteCode) {
195
+ globalThis.location.assign(`/${newWebsiteCode}${newPath || ''}`);
196
+ } else {
197
+ globalThis.location.assign(`${newPath || ''}`);
198
+ }
199
+ },
200
+ [availableStores, getPathname, searchParams]
201
+ );
202
+
203
+ const handleTriggerClick = useCallback(() => {
204
+ // Toggle Stores Menu.
205
+ setStoreMenuIsOpen(isOpen => !isOpen);
206
+ }, [setStoreMenuIsOpen]);
207
+
208
+ return {
209
+ availableStores,
210
+ currentGroupName,
211
+ currentWebsiteName,
212
+ storeGroups,
213
+ storeMenuRef,
214
+ storeMenuTriggerRef,
215
+ storeMenuIsOpen,
216
+ handleTriggerClick,
217
+ handleSwitchWebsite
218
+ };
219
+ };
@@ -0,0 +1,45 @@
1
+ import { gql } from '@apollo/client';
2
+
3
+ export const GET_STORE_CONFIG_DATA = gql`
4
+ query getStoreConfigData {
5
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
6
+ storeConfig {
7
+ store_code
8
+ store_name
9
+ store_group_name
10
+ website_name
11
+ }
12
+ }
13
+ `;
14
+
15
+ export const GET_ROUTE_DATA = gql`
16
+ query getRouteData($url: String!) {
17
+ route(url: $url) {
18
+ relative_url
19
+ }
20
+ }
21
+ `;
22
+
23
+ export const GET_AVAILABLE_WEBSITES_DATA = gql`
24
+ query availableStoresByUserIp {
25
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
26
+ availableStoresByUserIp {
27
+ default_display_currency_code
28
+ locale
29
+ secure_base_media_url
30
+ store_code
31
+ store_group_code
32
+ store_group_name
33
+ store_name
34
+ store_sort_order
35
+ website_name
36
+ website_code
37
+ }
38
+ }
39
+ `;
40
+
41
+ export default {
42
+ getStoreConfigData: GET_STORE_CONFIG_DATA,
43
+ getRouteData: GET_ROUTE_DATA,
44
+ getAvailableWebsitesData: GET_AVAILABLE_WEBSITES_DATA
45
+ };
@@ -0,0 +1,14 @@
1
+ import gql from 'graphql-tag';
2
+
3
+ export const GET_WEBSITE_BY_IP = gql`
4
+ query getWebsiteByUserIp (
5
+ $ip_address: String
6
+ ) {
7
+ getWebsiteByUserIp(ip_address: $ip_address) {
8
+ store_code
9
+ website_code
10
+ website_name
11
+ is_suggested
12
+ }
13
+ }
14
+ `;
@@ -0,0 +1,33 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import axios from 'axios';
3
+
4
+ export const useWebsiteByIp = websiteCodeInUrl => {
5
+ const [websiteByUserIpData, setWebsiteByUserIpData] = useState(null);
6
+
7
+ useEffect(() => {
8
+ if (!websiteCodeInUrl) {
9
+ const fetchData = async () => {
10
+ axios.get('https://extreme-ip-lookup.com/json/?key=NqwiAPsYZXoA3jwsnP7B').then((response) => {
11
+ let data = response.data;
12
+ if (data) {
13
+ setWebsiteByUserIpData(data)
14
+ }
15
+ }).catch((error) => {
16
+ console.log(error);
17
+ });
18
+ };
19
+
20
+ fetchData();
21
+ }
22
+ }, [websiteCodeInUrl]);
23
+
24
+ const getWebsiteByUserIp = useMemo(() => {
25
+ if (websiteByUserIpData) {
26
+ return websiteByUserIpData;
27
+ }
28
+ }, [websiteByUserIpData]);
29
+
30
+ return {
31
+ getWebsiteByUserIp
32
+ };
33
+ };