@graphcommerce/magento-store 9.1.0-canary.16 → 9.1.0-canary.18

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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +8 -0
  3. package/components/CurrencySymbol/CurrencySymbol.tsx +3 -2
  4. package/components/GlobalHead/GlobalHead.tsx +1 -1
  5. package/{Money.tsx → components/Money/Money.tsx} +1 -1
  6. package/{PageMeta.tsx → components/PageMeta/PageMeta.tsx} +1 -1
  7. package/components/StoreSwitcher/StoreSwitcherApply.tsx +16 -0
  8. package/components/StoreSwitcher/StoreSwitcherCurrencySelector.tsx +54 -0
  9. package/components/StoreSwitcher/StoreSwitcherGroupSelector.tsx +74 -0
  10. package/components/StoreSwitcher/StoreSwitcherList.graphql +21 -0
  11. package/components/{StoreSwitcherList → StoreSwitcher}/StoreSwitcherList.tsx +2 -6
  12. package/components/StoreSwitcher/StoreSwitcherStoreCurrencies.tsx +34 -0
  13. package/components/StoreSwitcher/StoreSwitcherStoreSelector.tsx +64 -0
  14. package/components/StoreSwitcher/index.ts +7 -0
  15. package/components/StoreSwitcher/useStoreSwitcher.tsx +203 -0
  16. package/components/StoreSwitcherButton/StoreSwitcherButton.tsx +12 -4
  17. package/docs/store-switcher-multiple-groups-one-store-multiple-currency.png +0 -0
  18. package/docs/store-switcher-multiple-groups-one-store-single-currency.png +0 -0
  19. package/docs/store-switcher-single-group-multiple-stores-multiple-currencies.png +0 -0
  20. package/docs/store-switcher-single-group-multiple-stores-single-currency.png +0 -0
  21. package/index.ts +8 -9
  22. package/mesh/resolvers.ts +14 -0
  23. package/package.json +10 -8
  24. package/plugins/magentoCurrencyCode.ts +35 -0
  25. package/plugins/magentoStoreGraphqlConfig.ts +8 -1
  26. package/plugins/meshMagentoStore.ts +23 -0
  27. package/queries/StoreConfig.graphql +6 -0
  28. package/queries/StoreConfigQueryFragment.graphql +6 -0
  29. package/schema/PrivateContext.graphqls +4 -0
  30. package/schema/StoreConfig-currency.graphqls +3 -0
  31. package/test/apolloClientStore.fixture.ts +1 -1
  32. package/utils/redirectOrNotFound.ts +2 -2
  33. package/StoreConfig.graphql +0 -5
  34. package/components/StoreSwitcherList/StoreSwitcherList.graphql +0 -10
  35. /package/{Money.graphql → components/Money/Money.graphql} +0 -0
  36. /package/{StoreConfigFragment.graphql → queries/StoreConfigFragment.graphql} +0 -0
  37. /package/{localeToStore.ts → utils/localeToStore.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Change Log
2
2
 
3
+ ## 9.1.0-canary.18
4
+
5
+ ## 9.1.0-canary.17
6
+
7
+ ### Minor Changes
8
+
9
+ - [#2496](https://github.com/graphcommerce-org/graphcommerce/pull/2496) [`4b6a65d`](https://github.com/graphcommerce-org/graphcommerce/commit/4b6a65db089d92492e7fde28a8e32bc41f5b9103) - Added support for multiple display currencies in the frontend. Multiple currencies were already supported, but this introduces Display Currencies for viewing the cart in different currencies. ([@paales](https://github.com/paales))
10
+
11
+ - [#2496](https://github.com/graphcommerce-org/graphcommerce/pull/2496) [`4b6a65d`](https://github.com/graphcommerce-org/graphcommerce/commit/4b6a65db089d92492e7fde28a8e32bc41f5b9103) - Refactored the Store Selector to be more of a form and have multiple nested toggles to switch groups, then stores and then currencies. It automatically hides features that aren't used: If only a single group is used with multiple stores only the store selector is shown. If multiple groups are used with each a single store is used, only the group selector is shown. If only a single currency is used, there is no currency selector. If multiple currencies are used, the currency selector is shown. This makes the selector more user-friendly and less cluttered. ([@paales](https://github.com/paales))
12
+
3
13
  ## 9.1.0-canary.16
4
14
 
5
15
  ## 9.1.0-canary.15
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # @graphcommerce/magento-store
2
+
3
+ ## Store Switcher
4
+
5
+ ![](./docs/store-switcher-single-group-multiple-stores-single-currency.png)
6
+ ![](./docs/store-switcher-single-group-multiple-stores-multiple-currencies.png)
7
+ ![](./docs/store-switcher-multiple-groups-one-store-single-currency.png)
8
+ ![](./docs/store-switcher-multiple-groups-one-store-multiple-currency.png)
@@ -3,10 +3,11 @@ import {
3
3
  CurrencySymbol as CurrencySymbolBase,
4
4
  type CurrencySymbolProps,
5
5
  } from '@graphcommerce/next-ui'
6
- import { StoreConfigDocument } from '../../StoreConfig.gql'
6
+ import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
7
7
 
8
8
  export function CurrencySymbol(props: CurrencySymbolProps) {
9
+ const { currency } = props
9
10
  const baseCurrencyCode = useQuery(StoreConfigDocument).data?.storeConfig?.base_currency_code ?? ''
10
11
 
11
- return <CurrencySymbolBase {...props} currency={baseCurrencyCode} />
12
+ return <CurrencySymbolBase {...props} currency={currency || baseCurrencyCode} />
12
13
  }
@@ -1,7 +1,7 @@
1
1
  import { useQuery } from '@graphcommerce/graphql'
2
2
  import type { GlobalHeadProps as GlobalHeadPropsBase } from '@graphcommerce/next-ui'
3
3
  import { GlobalHead as GlobalHeadBase } from '@graphcommerce/next-ui'
4
- import { StoreConfigDocument } from '../../StoreConfig.gql'
4
+ import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
5
5
 
6
6
  export type GlobalHeadProps = Omit<GlobalHeadPropsBase, 'name'>
7
7
 
@@ -2,8 +2,8 @@ import { useQuery } from '@graphcommerce/graphql'
2
2
  import type { CurrencyFormatProps } from '@graphcommerce/next-ui'
3
3
  import { CurrencyFormat } from '@graphcommerce/next-ui'
4
4
  import type { SxProps, Theme } from '@mui/material'
5
+ import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
5
6
  import type { MoneyFragment } from './Money.gql'
6
- import { StoreConfigDocument } from './StoreConfig.gql'
7
7
 
8
8
  type OverridableProps = {
9
9
  round?: boolean
@@ -1,7 +1,7 @@
1
1
  import { useQuery } from '@graphcommerce/graphql'
2
2
  import type { PageMetaProps as NextPageMetaProps } from '@graphcommerce/next-ui'
3
3
  import { PageMeta as NextPageMeta } from '@graphcommerce/next-ui'
4
- import { StoreConfigDocument } from './StoreConfig.gql'
4
+ import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
5
5
 
6
6
  export type PageMetaProps = Omit<NextPageMetaProps, 'canonical'> & {
7
7
  canonical?: string
@@ -0,0 +1,16 @@
1
+ import { useFormState } from '@graphcommerce/ecommerce-ui'
2
+ import type { ButtonProps, LinkOrButtonProps } from '@graphcommerce/next-ui'
3
+ import { Button, LinkOrButton } from '@graphcommerce/next-ui'
4
+ import { useStoreSwitcherForm } from './useStoreSwitcher'
5
+
6
+ export function StoreSwitcherApplyButton(props: ButtonProps<'button'>) {
7
+ const { control } = useStoreSwitcherForm()
8
+ const formState = useFormState({ control })
9
+ return <Button type='submit' loading={formState.isSubmitting} {...props} />
10
+ }
11
+
12
+ export function StoreSwitcherLinkOrButton(props: LinkOrButtonProps) {
13
+ const { control } = useStoreSwitcherForm()
14
+ const formState = useFormState({ control })
15
+ return <LinkOrButton type='submit' loading={formState.isSubmitting} {...props} />
16
+ }
@@ -0,0 +1,54 @@
1
+ import type { ActionCardListFormProps } from '@graphcommerce/ecommerce-ui'
2
+ import { ActionCardListForm, useWatch } from '@graphcommerce/ecommerce-ui'
3
+ import { useIsomorphicLayoutEffect } from '@graphcommerce/framer-utils'
4
+ import type { ActionCardProps } from '@graphcommerce/next-ui'
5
+ import { ActionCard, CurrencySymbol, nonNullable, sxx } from '@graphcommerce/next-ui'
6
+ import type { StoreSwitcherFormValues } from './useStoreSwitcher'
7
+ import { useSelectedStore, useStoreSwitcherForm } from './useStoreSwitcher'
8
+
9
+ export type StoreSwitcherCurrencySelectorProps = {
10
+ header?: React.ReactNode
11
+ } & Omit<
12
+ ActionCardListFormProps<ActionCardProps, StoreSwitcherFormValues>,
13
+ 'name' | 'control' | 'items' | 'required' | 'render'
14
+ >
15
+
16
+ export function StoreSwitcherCurrencySelector(props: StoreSwitcherCurrencySelectorProps) {
17
+ const { header, ...actionCardList } = props
18
+ const { control } = useStoreSwitcherForm()
19
+ const store = useSelectedStore()
20
+
21
+ const currencies = store?.currency?.available_currency_codes?.filter(nonNullable) ?? []
22
+ const defaultValue = store?.currency?.default_display_currency_code ?? currencies?.[0] ?? ''
23
+
24
+ // Make sure the default currency is the first in the list
25
+ currencies.sort((a, b) => {
26
+ if (a === defaultValue) return -1
27
+ if (b === defaultValue) return 1
28
+ return 0
29
+ })
30
+
31
+ const hidden = currencies.length === 1
32
+
33
+ return (
34
+ <>
35
+ {!hidden && header}
36
+ <ActionCardListForm<ActionCardProps, StoreSwitcherFormValues>
37
+ control={control}
38
+ defaultValue={defaultValue}
39
+ name='currency'
40
+ layout='stack'
41
+ size='responsive'
42
+ required
43
+ color='secondary'
44
+ render={ActionCard}
45
+ sx={sxx(hidden && { display: 'none !important' })}
46
+ items={currencies.map((currency) => ({
47
+ value: currency,
48
+ title: <CurrencySymbol currency={currency} variant='full' />,
49
+ }))}
50
+ {...actionCardList}
51
+ />
52
+ </>
53
+ )
54
+ }
@@ -0,0 +1,74 @@
1
+ import type { ActionCardListFormProps } from '@graphcommerce/ecommerce-ui'
2
+ import { ActionCardListForm } from '@graphcommerce/ecommerce-ui'
3
+ import type { ActionCardProps } from '@graphcommerce/next-ui'
4
+ import { ActionCard, FlagAvatar, ListFormat, sxx } from '@graphcommerce/next-ui'
5
+ import React from 'react'
6
+ import type { StoreSwitcherStoreCurrenciesProps } from './StoreSwitcherStoreCurrencies'
7
+ import { StoreSwitcherStoreCurrencies } from './StoreSwitcherStoreCurrencies'
8
+ import type { StoreSwitcherFormValues, StoreSwitcherGroup } from './useStoreSwitcher'
9
+ import { useStoreSwitcherForm } from './useStoreSwitcher'
10
+
11
+ export type StoreSwitcherGroupSelectorProps = {
12
+ header?: React.ReactNode
13
+ showStores?: number
14
+ } & Omit<StoreSwitcherStoreCurrenciesProps, 'store'> &
15
+ Omit<
16
+ ActionCardListFormProps<
17
+ ActionCardProps & { group?: StoreSwitcherGroup },
18
+ StoreSwitcherFormValues
19
+ >,
20
+ 'name' | 'control' | 'items' | 'required' | 'render'
21
+ >
22
+
23
+ export function StoreSwitcherGroupSelector(props: StoreSwitcherGroupSelectorProps) {
24
+ const { header, showStores = 0, showCurrencies, ...actionCardList } = props
25
+ const { control, storeGroups } = useStoreSwitcherForm()
26
+
27
+ const hidden = storeGroups.length === 1 && storeGroups[0].stores.length > 1
28
+
29
+ return (
30
+ <>
31
+ {!hidden && header}
32
+ <ActionCardListForm<ActionCardProps, StoreSwitcherFormValues>
33
+ control={control}
34
+ name='storeGroupCode'
35
+ layout='stack'
36
+ size='responsive'
37
+ required
38
+ color='secondary'
39
+ render={ActionCard}
40
+ sx={sxx(hidden && { display: 'none !important' })}
41
+ items={storeGroups.map((group) => ({
42
+ group,
43
+ image: group.country ? <FlagAvatar country={group.country} size='40px' /> : undefined,
44
+ title: <>{group.store_group_name}</>,
45
+ details: showStores && (
46
+ <>
47
+ {group.stores.length <= showStores && (
48
+ <ListFormat listStyle='short' type='unit'>
49
+ {group.stores.map((store) => (
50
+ <React.Fragment key={store.store_code}>
51
+ {store.store_name}
52
+ <StoreSwitcherStoreCurrencies
53
+ store={store}
54
+ showCurrencies={group.stores.length > 1 ? 0 : showCurrencies}
55
+ brackets
56
+ />
57
+ </React.Fragment>
58
+ ))}
59
+ </ListFormat>
60
+ )}
61
+ </>
62
+ ),
63
+ disabled: group.disabled,
64
+ value: group.store_group_code,
65
+ slotProps: {
66
+ title: { sx: { typography: 'subtitle1' } },
67
+ details: { sx: { typography: 'body1', color: 'text.secondary' } },
68
+ },
69
+ }))}
70
+ {...actionCardList}
71
+ />
72
+ </>
73
+ )
74
+ }
@@ -0,0 +1,21 @@
1
+ query StoreSwitcherList {
2
+ availableStores {
3
+ store_name
4
+ store_code
5
+ locale
6
+ base_currency_code
7
+ store_group_name
8
+ store_group_code
9
+ currency {
10
+ available_currency_codes
11
+ base_currency_code
12
+ base_currency_symbol
13
+ default_display_currency_code
14
+ default_display_currency_symbol
15
+ exchange_rates {
16
+ currency_to
17
+ rate
18
+ }
19
+ }
20
+ }
21
+ }
@@ -3,7 +3,7 @@ import { extendableComponent, FlagAvatar, NextLink } from '@graphcommerce/next-u
3
3
  import type { SxProps, Theme } from '@mui/material'
4
4
  import { Collapse, List, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material'
5
5
  import React from 'react'
6
- import { localeToStore, storeToLocale } from '../../localeToStore'
6
+ import { localeToStore, storeToLocale } from '../../utils/localeToStore'
7
7
  import type { StoreSwitcherListQuery } from './StoreSwitcherList.gql'
8
8
 
9
9
  type Store = NonNullable<NonNullable<StoreSwitcherListQuery['availableStores']>[0]>
@@ -56,11 +56,7 @@ export function StoreSwitcherList(props: StoreSwitcherListProps) {
56
56
  })}
57
57
  >
58
58
  <ListItemAvatar>
59
- <FlagAvatar
60
- country={code}
61
- className={classes.avatar}
62
- sx={{ width: 30, height: 30 }}
63
- />
59
+ <FlagAvatar country={code} className={classes.avatar} size='30px' />
64
60
  </ListItemAvatar>
65
61
  <ListItemText>
66
62
  {group.name}
@@ -0,0 +1,34 @@
1
+ import type { CurrencySymbolProps } from '@graphcommerce/next-ui'
2
+ import { CurrencySymbol, ListFormat, nonNullable } from '@graphcommerce/next-ui'
3
+ import type { StoreSwitcherStore } from './useStoreSwitcher'
4
+
5
+ export type StoreSwitcherStoreCurrenciesProps = {
6
+ showCurrencies?: number
7
+ store: StoreSwitcherStore
8
+ brackets?: boolean
9
+ variant?: CurrencySymbolProps['variant']
10
+ }
11
+
12
+ export function StoreSwitcherStoreCurrencies(props: StoreSwitcherStoreCurrenciesProps) {
13
+ const { store, showCurrencies = 0, brackets = false, variant } = props
14
+
15
+ const currencies = (store.currency?.available_currency_codes ?? [])?.filter(nonNullable)
16
+
17
+ const show = currencies.length <= showCurrencies
18
+ const list = (
19
+ <ListFormat listStyle='short' type='unit'>
20
+ {currencies.map((currency) => (
21
+ <CurrencySymbol
22
+ key={currency}
23
+ compactDisplay='short'
24
+ variant={variant}
25
+ currency={currency}
26
+ />
27
+ ))}
28
+ </ListFormat>
29
+ )
30
+
31
+ if (!show) return null
32
+ if (!brackets) return list
33
+ return <> ({list})</>
34
+ }
@@ -0,0 +1,64 @@
1
+ import type { ActionCardListFormProps } from '@graphcommerce/ecommerce-ui'
2
+ import { ActionCardListForm } from '@graphcommerce/ecommerce-ui'
3
+ import type { ActionCardProps } from '@graphcommerce/next-ui'
4
+ import { ActionCard, sxx } from '@graphcommerce/next-ui'
5
+ import type { StoreSwitcherStoreCurrenciesProps } from './StoreSwitcherStoreCurrencies'
6
+ import { StoreSwitcherStoreCurrencies } from './StoreSwitcherStoreCurrencies'
7
+ import type { StoreSwitcherFormValues, StoreSwitcherStore } from './useStoreSwitcher'
8
+ import { useSelectedStoreGroup, useStoreSwitcherForm } from './useStoreSwitcher'
9
+
10
+ export type StoreSwitcherSelectorProps = { header?: React.ReactNode } & Omit<
11
+ StoreSwitcherStoreCurrenciesProps,
12
+ 'store'
13
+ > &
14
+ Omit<
15
+ ActionCardListFormProps<
16
+ ActionCardProps & { store?: StoreSwitcherStore },
17
+ StoreSwitcherFormValues
18
+ >,
19
+ 'name' | 'control' | 'items' | 'required' | 'render'
20
+ >
21
+
22
+ export function StoreSwitcherStoreSelector(props: StoreSwitcherSelectorProps) {
23
+ const { header, showCurrencies, ...actionCardProps } = props
24
+ const { control } = useStoreSwitcherForm()
25
+ const selectedGroup = useSelectedStoreGroup()
26
+
27
+ const hidden = selectedGroup?.stores.length === 1
28
+
29
+ return (
30
+ <>
31
+ {!hidden && header}
32
+ <ActionCardListForm<ActionCardProps, StoreSwitcherFormValues>
33
+ control={control}
34
+ defaultValue={selectedGroup?.stores?.[0].store_code ?? undefined}
35
+ shouldUnregister
36
+ name='storeCode'
37
+ layout='stack'
38
+ size='responsive'
39
+ required
40
+ render={ActionCard}
41
+ color='secondary'
42
+ sx={sxx(hidden && { display: 'none !important' })}
43
+ items={(selectedGroup?.stores ?? []).map((store) => ({
44
+ store,
45
+ title: store.store_name,
46
+ details: (
47
+ <StoreSwitcherStoreCurrencies
48
+ store={store}
49
+ showCurrencies={showCurrencies}
50
+ variant='full'
51
+ />
52
+ ),
53
+ value: store.store_code,
54
+ disabled: store.disabled,
55
+ slotProps: {
56
+ title: { sx: { typography: 'subtitle1' } },
57
+ details: { sx: { typography: 'body1', color: 'text.secondary' } },
58
+ },
59
+ }))}
60
+ {...actionCardProps}
61
+ />
62
+ </>
63
+ )
64
+ }
@@ -0,0 +1,7 @@
1
+ export * from './StoreSwitcherApply'
2
+ export * from './StoreSwitcherCurrencySelector'
3
+ export * from './StoreSwitcherGroupSelector'
4
+ export * from './StoreSwitcherList'
5
+ export * from './StoreSwitcherList.gql'
6
+ export * from './StoreSwitcherStoreSelector'
7
+ export * from './useStoreSwitcher'
@@ -0,0 +1,203 @@
1
+ import type { SubmitHandler } from '@graphcommerce/ecommerce-ui'
2
+ import { useForm, useWatch, type UseFormReturn } from '@graphcommerce/ecommerce-ui'
3
+ import { useIsomorphicLayoutEffect } from '@graphcommerce/framer-utils'
4
+ import {
5
+ cookie,
6
+ filterNonNullableKeys,
7
+ storefrontAll,
8
+ useStorefrontConfig,
9
+ type RequiredKeys,
10
+ } from '@graphcommerce/next-ui'
11
+ import { useRouter } from 'next/router'
12
+ import { createContext, useContext, useEffect, useLayoutEffect, useMemo } from 'react'
13
+ import { storeToLocale } from '../../utils/localeToStore'
14
+ import { type StoreSwitcherListQuery } from './StoreSwitcherList.gql'
15
+
16
+ export type StoreSwitcherStore = RequiredKeys<
17
+ NonNullable<NonNullable<StoreSwitcherListQuery['availableStores']>[0]>,
18
+ 'store_code' | 'store_name' | 'store_group_name' | 'store_group_code'
19
+ > & {
20
+ country: string
21
+ disabled: boolean
22
+ }
23
+
24
+ export type StoreSwitcherGroup = Pick<
25
+ StoreSwitcherStore,
26
+ 'store_group_code' | 'store_group_name'
27
+ > & {
28
+ stores: StoreSwitcherStore[]
29
+ country?: string
30
+ disabled: boolean
31
+ }
32
+
33
+ export type StoreSwitcherFormValues = {
34
+ storeGroupCode: string
35
+ storeCode: string
36
+ currency: string
37
+ }
38
+
39
+ type StoreSwitcherFormContextType = {
40
+ stores: StoreSwitcherStore[]
41
+ storeGroups: StoreSwitcherGroup[]
42
+ } & UseFormReturn<StoreSwitcherFormValues>
43
+
44
+ const StoreSwitcherFormContext = createContext<StoreSwitcherFormContextType | null>(null)
45
+
46
+ export function useStoreSwitcherForm(
47
+ context?: StoreSwitcherFormContextType,
48
+ ): StoreSwitcherFormContextType {
49
+ const ctx = useContext(StoreSwitcherFormContext) ?? context
50
+ if (!ctx) throw new Error('useStoreSwitcherForm must be used within a StoreSwitcherFormProvider')
51
+ return ctx
52
+ }
53
+
54
+ export function useSelectedStoreGroup(
55
+ context?: StoreSwitcherFormContextType,
56
+ ): StoreSwitcherGroup | null {
57
+ const { storeGroups, control } = useStoreSwitcherForm(context)
58
+
59
+ const selectedStoreGroupCode = useWatch({ control, name: 'storeGroupCode' })
60
+ const selectedStoreGroup = storeGroups.find(
61
+ (group) => group.store_group_code === selectedStoreGroupCode,
62
+ )
63
+
64
+ return selectedStoreGroup ?? null
65
+ }
66
+
67
+ export function useSelectedStore(
68
+ context?: StoreSwitcherFormContextType,
69
+ ): StoreSwitcherStore | null {
70
+ const { control } = useStoreSwitcherForm(context)
71
+
72
+ const selectedStoreGroup = useSelectedStoreGroup(context)
73
+
74
+ const selectedStoreCode = useWatch({ control, name: 'storeCode' })
75
+ const selectedStore = selectedStoreGroup?.stores?.find(
76
+ (store) => store.store_code === selectedStoreCode,
77
+ )
78
+ return selectedStore ?? null
79
+ }
80
+
81
+ export function useSelectedCurrency(context?: StoreSwitcherFormContextType): string | null {
82
+ const { control } = useStoreSwitcherForm(context)
83
+
84
+ const selectedStore = useSelectedStore(context)
85
+ const selectedCurrencyCode = useWatch({ control, name: 'currency' })
86
+
87
+ return selectedStore?.currency?.available_currency_codes?.includes(selectedCurrencyCode)
88
+ ? selectedCurrencyCode
89
+ : null
90
+ }
91
+
92
+ export function useStoreSwitcher({
93
+ availableStores,
94
+ }: StoreSwitcherListQuery): StoreSwitcherFormContextType {
95
+ const stores: StoreSwitcherStore[] = useMemo(
96
+ () =>
97
+ filterNonNullableKeys(availableStores, [
98
+ 'store_name',
99
+ 'store_code',
100
+ 'store_group_code',
101
+ 'store_group_name',
102
+ 'base_currency_code',
103
+ 'locale',
104
+ ])
105
+ .map((store) => ({
106
+ ...store,
107
+ country: store.locale.split('_')[1]?.toLowerCase(),
108
+ disabled: !storefrontAll.find((l) => l.magentoStoreCode === store.store_code),
109
+ }))
110
+ .filter((store) => !store.disabled),
111
+ [availableStores],
112
+ )
113
+
114
+ const storeGroups = Object.values(
115
+ stores.reduce<{
116
+ [group: string]: StoreSwitcherGroup
117
+ }>((storesGrouped, store) => {
118
+ if (!storesGrouped[store.store_group_code]) {
119
+ const group: StoreSwitcherGroup = {
120
+ store_group_code: store.store_group_code,
121
+ store_group_name: store.store_group_name,
122
+ stores: [],
123
+ disabled: true,
124
+ country: store.country,
125
+ }
126
+ storesGrouped[store.store_group_code] = group
127
+ }
128
+ if (!store.disabled) storesGrouped[store.store_group_code].disabled = false
129
+ if (storesGrouped[store.store_group_code].country !== store.country) {
130
+ storesGrouped[store.store_group_code].country = undefined
131
+ }
132
+
133
+ storesGrouped[store.store_group_code].stores.push(store)
134
+ return storesGrouped
135
+ }, {}),
136
+ )
137
+
138
+ const storeCode = useStorefrontConfig().magentoStoreCode
139
+
140
+ const form = useForm<StoreSwitcherFormValues>({
141
+ defaultValues: {
142
+ storeCode,
143
+ storeGroupCode:
144
+ stores.find((store) => store.store_code === storeCode)?.store_group_code ?? '',
145
+ },
146
+ })
147
+
148
+ return { storeGroups, stores, ...form }
149
+ }
150
+
151
+ export function StoreSwitcherFormProvider(
152
+ props: {
153
+ children?: React.ReactNode
154
+ onSubmit?: SubmitHandler<StoreSwitcherFormValues>
155
+ } & StoreSwitcherListQuery,
156
+ ) {
157
+ const { children, onSubmit, availableStores } = props
158
+ const context = useStoreSwitcher({ availableStores })
159
+ const { setValue, storeGroups } = context
160
+
161
+ const selectedStoreGroup = useSelectedStoreGroup(context) ?? storeGroups[0]
162
+ const selectedStore = useSelectedStore(context)
163
+ const selectedCurrency = useSelectedCurrency(context)
164
+
165
+ useIsomorphicLayoutEffect(() => {
166
+ const currency = cookie('Magento-Content-Currency')
167
+ if (currency) setValue('currency', currency)
168
+ }, [setValue])
169
+
170
+ useIsomorphicLayoutEffect(() => {
171
+ const defaultStore = selectedStoreGroup.stores[0]
172
+ if (!selectedStore) {
173
+ setValue('storeCode', defaultStore.store_code)
174
+ }
175
+
176
+ const currencyCode =
177
+ selectedStore?.currency?.default_display_currency_code ??
178
+ defaultStore.currency?.default_display_currency_code
179
+
180
+ if (!selectedCurrency && currencyCode) {
181
+ setValue('currency', currencyCode)
182
+ }
183
+ }, [selectedCurrency, selectedStore, selectedStoreGroup.stores, setValue])
184
+
185
+ const submit = context.handleSubmit(async (data, event) => {
186
+ if (
187
+ data.currency ===
188
+ context.stores.find((store) => store.store_code === data.storeCode)?.base_currency_code
189
+ ) {
190
+ cookie('Magento-Content-Currency', null)
191
+ } else {
192
+ cookie('Magento-Content-Currency', data.currency)
193
+ }
194
+
195
+ onSubmit?.(data, event)
196
+ })
197
+
198
+ return (
199
+ <StoreSwitcherFormContext.Provider value={context}>
200
+ <form onSubmit={submit}>{children}</form>
201
+ </StoreSwitcherFormContext.Provider>
202
+ )
203
+ }
@@ -1,9 +1,10 @@
1
1
  import { useQuery } from '@graphcommerce/graphql'
2
- import { extendableComponent, FlagAvatar } from '@graphcommerce/next-ui'
2
+ import { cookie, extendableComponent, FlagAvatar } from '@graphcommerce/next-ui'
3
3
  import type { SxProps, Theme } from '@mui/material'
4
4
  import { Button } from '@mui/material'
5
5
  import { useRouter } from 'next/router'
6
- import { StoreConfigDocument } from '../../StoreConfig.gql'
6
+ import { useMemo } from 'react'
7
+ import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
7
8
 
8
9
  export type StoreSwitcherButtonProps = { sx?: SxProps<Theme> }
9
10
 
@@ -13,10 +14,16 @@ const { classes } = extendableComponent(name, parts)
13
14
 
14
15
  export function StoreSwitcherButton(props: StoreSwitcherButtonProps) {
15
16
  const { sx } = props
17
+
16
18
  const config = useQuery(StoreConfigDocument)
17
19
  const country = config.data?.storeConfig?.locale?.split('_')?.[1]?.toLowerCase() ?? ''
18
20
  const router = useRouter()
19
21
 
22
+ const currency = useMemo(
23
+ () => cookie('Magento-Content-Currency') ?? config.data?.storeConfig?.base_currency_code,
24
+ [config.data?.storeConfig?.base_currency_code],
25
+ )
26
+
20
27
  return (
21
28
  <Button
22
29
  variant='text'
@@ -28,9 +35,10 @@ export function StoreSwitcherButton(props: StoreSwitcherButtonProps) {
28
35
  <FlagAvatar
29
36
  country={country}
30
37
  className={classes.avatar}
31
- sx={{ height: 20, width: 20, marginRight: '10px' }}
38
+ size='20px'
39
+ sx={{ marginRight: '10px' }}
32
40
  />
33
- {config.data?.storeConfig?.store_name} - {config.data?.storeConfig?.base_currency_code}
41
+ {config.data?.storeConfig?.store_name} {currency}
34
42
  </Button>
35
43
  )
36
44
  }
package/index.ts CHANGED
@@ -1,14 +1,13 @@
1
+ export * from './components/CurrencySymbol/CurrencySymbol'
1
2
  export * from './components/GlobalHead/GlobalHead'
3
+ export * from './components/Money/Money'
4
+ export * from './components/Money/Money.gql'
5
+ export * from './components/PageMeta/PageMeta'
6
+ export * from './components/StoreSwitcherButton/StoreSwitcherButton'
2
7
  export * from './hooks/useFindCountry'
3
8
  export * from './hooks/useFindRegion'
4
- export * from './localeToStore'
5
- export * from './Money'
6
- export * from './Money.gql'
7
- export * from './PageMeta'
8
9
  export * from './queries/CountryRegions.gql'
9
- export * from './StoreConfig.gql'
10
- export * from './components/StoreSwitcherButton/StoreSwitcherButton'
11
- export * from './components/StoreSwitcherList/StoreSwitcherList'
12
- export * from './components/StoreSwitcherList/StoreSwitcherList.gql'
10
+ export * from './queries/StoreConfig.gql'
11
+ export * from './utils/localeToStore'
13
12
  export * from './utils/redirectOrNotFound'
14
- export * from './components/CurrencySymbol/CurrencySymbol'
13
+ export * from './components/StoreSwitcher'
@@ -0,0 +1,14 @@
1
+ import type { Resolvers } from '@graphcommerce/graphql-mesh'
2
+
3
+ export const resolvers: Resolvers = {
4
+ StoreConfig: {
5
+ currency: {
6
+ selectionSet: '{ store_code }',
7
+ resolve: (parent, args, ctx, info) => {
8
+ if (!parent.store_code) return null
9
+ const context = { ...ctx, headers: { store: parent.store_code } }
10
+ return context.m2.Query.currency({ context, info }) ?? null
11
+ },
12
+ },
13
+ },
14
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-store",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.1.0-canary.16",
5
+ "version": "9.1.0-canary.18",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -12,13 +12,15 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.16",
16
- "@graphcommerce/graphql": "^9.1.0-canary.16",
17
- "@graphcommerce/graphql-mesh": "^9.1.0-canary.16",
18
- "@graphcommerce/image": "^9.1.0-canary.16",
19
- "@graphcommerce/next-ui": "^9.1.0-canary.16",
20
- "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.16",
21
- "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.16",
15
+ "@graphcommerce/ecommerce-ui": "^9.1.0-canary.18",
16
+ "@graphcommerce/eslint-config-pwa": "^9.1.0-canary.18",
17
+ "@graphcommerce/framer-utils": "^9.1.0-canary.18",
18
+ "@graphcommerce/graphql": "^9.1.0-canary.18",
19
+ "@graphcommerce/graphql-mesh": "^9.1.0-canary.18",
20
+ "@graphcommerce/image": "^9.1.0-canary.18",
21
+ "@graphcommerce/next-ui": "^9.1.0-canary.18",
22
+ "@graphcommerce/prettier-config-pwa": "^9.1.0-canary.18",
23
+ "@graphcommerce/typescript-config-pwa": "^9.1.0-canary.18",
22
24
  "@lingui/core": "^4.2.1",
23
25
  "@lingui/macro": "^4.2.1",
24
26
  "@lingui/react": "^4.2.1",
@@ -0,0 +1,35 @@
1
+ import {
2
+ type getPrivateQueryContext as getPrivateQueryContextType,
3
+ type usePrivateQueryContext as usePrivateQueryContextType,
4
+ } from '@graphcommerce/graphql'
5
+ import type { PrivateContext } from '@graphcommerce/graphql-mesh'
6
+ import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
7
+ import { cookie } from '@graphcommerce/next-ui'
8
+
9
+ export const config: PluginConfig = {
10
+ type: 'function',
11
+ module: '@graphcommerce/graphql',
12
+ }
13
+
14
+ export const getPrivateQueryContext: FunctionPlugin<typeof getPrivateQueryContextType> = (
15
+ prev,
16
+ client,
17
+ ...args
18
+ ) => {
19
+ const currencyCode = cookie('Magento-Content-Currency')
20
+
21
+ const res = prev(client, ...args)
22
+ if (!currencyCode) return res
23
+ return { ...res, currencyCode } satisfies PrivateContext
24
+ }
25
+
26
+ export const usePrivateQueryContext: FunctionPlugin<typeof usePrivateQueryContextType> = (
27
+ prev,
28
+ ...args
29
+ ) => {
30
+ const currencyCode = cookie('Magento-Content-Currency')
31
+
32
+ const res = prev(...args)
33
+ if (!currencyCode) return res
34
+ return { ...res, currencyCode }
35
+ }
@@ -1,5 +1,6 @@
1
1
  import { setContext, type graphqlConfig as graphqlConfigType } from '@graphcommerce/graphql'
2
2
  import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
3
+ import { cookie } from '@graphcommerce/next-ui'
3
4
 
4
5
  export const config: PluginConfig = {
5
6
  type: 'function',
@@ -15,7 +16,13 @@ export const graphqlConfig: FunctionPlugin<typeof graphqlConfigType> = (prev, co
15
16
  ...results.links,
16
17
  setContext((_, context) => {
17
18
  if (!context.headers) context.headers = {}
18
- context.headers.store = conf.storefront.magentoStoreCode
19
+ if (!context.headers.store) {
20
+ context.headers.store = conf.storefront.magentoStoreCode
21
+ }
22
+
23
+ const contentCurrency = cookie('Magento-Content-Currency')
24
+ if (contentCurrency && typeof context.headers['content-currency'] === 'undefined')
25
+ context.headers['content-currency'] = contentCurrency
19
26
 
20
27
  if (conf.preview) {
21
28
  // To disable caching from the backend, we provide a bogus cache ID.
@@ -0,0 +1,23 @@
1
+ import type { meshConfig as meshConfigBase } from '@graphcommerce/graphql-mesh/meshConfig'
2
+ import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
3
+
4
+ export const config: PluginConfig = {
5
+ module: '@graphcommerce/graphql-mesh/meshConfig',
6
+ type: 'function',
7
+ }
8
+
9
+ export const meshConfig: FunctionPlugin<typeof meshConfigBase> = (
10
+ prev,
11
+ baseConfig,
12
+ graphCommerceConfig,
13
+ ) =>
14
+ prev(
15
+ {
16
+ ...baseConfig,
17
+ additionalResolvers: [
18
+ ...(baseConfig.additionalResolvers ?? []),
19
+ '@graphcommerce/magento-store/mesh/resolvers.ts',
20
+ ],
21
+ },
22
+ graphCommerceConfig,
23
+ )
@@ -0,0 +1,6 @@
1
+ query StoreConfig {
2
+ storeConfig {
3
+ store_code
4
+ }
5
+ ...StoreConfigQueryFragment
6
+ }
@@ -0,0 +1,6 @@
1
+ fragment StoreConfigQueryFragment on Query {
2
+ __typename
3
+ storeConfig {
4
+ ...StoreConfigFragment
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ input PrivateContext {
2
+ storeCode: String
3
+ currencyCode: String
4
+ }
@@ -0,0 +1,3 @@
1
+ extend type StoreConfig {
2
+ currency: Currency
3
+ }
@@ -4,7 +4,7 @@
4
4
  import type { NormalizedCacheObject } from '@graphcommerce/graphql'
5
5
  import { ApolloClient, InMemoryCache } from '@graphcommerce/graphql'
6
6
  import { test as base } from '@playwright/test'
7
- import { localeToStore } from '../localeToStore'
7
+ import { localeToStore } from '../utils/localeToStore'
8
8
 
9
9
  type ApolloClientStoreTest = {
10
10
  apolloClient: ApolloClient<NormalizedCacheObject>
@@ -3,10 +3,10 @@ import type { ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@gr
3
3
  import { flushMeasurePerf } from '@graphcommerce/graphql'
4
4
  import { isTypename, nonNullable, storefrontConfig } from '@graphcommerce/next-ui'
5
5
  import type { Redirect } from 'next'
6
- import { defaultLocale } from '../localeToStore'
7
- import type { StoreConfigQuery } from '../StoreConfig.gql'
6
+ import type { StoreConfigQuery } from '../queries/StoreConfig.gql'
8
7
  import type { HandleRedirectQuery } from './HandleRedirect.gql'
9
8
  import { HandleRedirectDocument } from './HandleRedirect.gql'
9
+ import { defaultLocale } from './localeToStore'
10
10
 
11
11
  export type RedirectOr404Return = Promise<
12
12
  | { redirect: Redirect; revalidate?: number | boolean }
@@ -1,5 +0,0 @@
1
- query StoreConfig {
2
- storeConfig {
3
- ...StoreConfigFragment
4
- }
5
- }
@@ -1,10 +0,0 @@
1
- query StoreSwitcherList {
2
- availableStores {
3
- store_name
4
- store_code
5
- locale
6
- base_currency_code
7
- store_group_name
8
- store_group_code
9
- }
10
- }
File without changes