Skip to content

Commit

Permalink
Refactored the Store Selector to be more of a form and have multiple …
Browse files Browse the repository at this point in the history
…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.

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.
  • Loading branch information
paales committed Feb 10, 2025
1 parent a261149 commit 4b6a65d
Show file tree
Hide file tree
Showing 49 changed files with 796 additions and 86 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-pans-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/magento-store': minor
---

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.
5 changes: 5 additions & 0 deletions .changeset/lemon-ads-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/magento-store': minor
---

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.
13 changes: 13 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ roadmap.
- [x] Adobe Commerce: Store credit functionality
- [x] Adobe Commerce: Gift card functionality
- [ ] Adobe Commerce: Content staging functionality in preview mode.
- [ ] Custom options pricing on product page
- [ ] Storelocator UI. Generic Store Locator UI to allow for any datasource to
be used. Integrate with Magento's pickupLocations functionality.
- [ ] Store inventory on product pages UI and Magento's pickupLocations
Expand All @@ -30,6 +31,11 @@ roadmap.
GraphCommerce integration.
- [ ] GraphCommerce Pro: Cache Notifier module + GraphCommerce Integration to
revalidate the frontend on backend changes.
- [ ] GraphCommerce: Video support
- [ ] GraphCommerce: Product List Gallery
- [ ] GraphCommerce: Product List Configurable Image Toggle
- [x] Magento: Implement currency query and switch frontend to different
currency with usePrivateQuery

## Researching / Considering

Expand Down Expand Up @@ -69,6 +75,13 @@ roadmap.
integration)
- [ ] GraphCommerce Pro: Shop in shop functionality
- [ ] GraphCommerce POS: Create a light POS integration for GraphCommerce
- [ ] Magento customerDownloadableProducts
- [ ] Magento: customerPaymentTokens + deletePaymentToken
- [ ] Magento: customerDownloadableProducts
- [ ] Magento 2.4.7: recaptchaV3Config
- [ ] Magento: downloadable products
- [ ] Magento: Migrate to createCustomerV2
- [ ] Magento 2.4.7: createGuestCart migration

## Released

Expand Down
68 changes: 54 additions & 14 deletions examples/magento-graphcms/pages/switch-stores.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,92 @@
import { PageOptions } from '@graphcommerce/framer-next-pages'
import { PageOptions, usePrevPageRouter } from '@graphcommerce/framer-next-pages'
import {
PageMeta,
StoreConfigDocument,
StoreSwitcherList,
StoreSwitcherApplyButton,
StoreSwitcherCurrencySelector,
StoreSwitcherFormProvider,
StoreSwitcherGroupSelector,
StoreSwitcherLinkOrButton,
StoreSwitcherListDocument,
StoreSwitcherListQuery,
StoreSwitcherStoreSelector,
storeToLocale,
} from '@graphcommerce/magento-store'
import {
FormActions,
GetStaticProps,
iconLanguage,
LayoutOverlayHeader,
LayoutTitle,
OverlayStickyBottom,
SectionHeader,
} from '@graphcommerce/next-ui'
import { i18n } from '@lingui/core'
import { Trans } from '@lingui/react'
import { Container } from '@mui/material'
import { useRouter } from 'next/router'
import { LayoutOverlay, LayoutOverlayProps } from '../components'
import { LayoutDocument, LayoutOverlay, LayoutOverlayProps } from '../components'
import { graphqlSsrClient, graphqlSharedClient } from '../lib/graphql/graphqlSsrClient'
import { useRouter } from 'next/router'

type RouteProps = { country?: string[] }
type Props = StoreSwitcherListQuery
type GetPageStaticProps = GetStaticProps<LayoutOverlayProps, Props, RouteProps>

function StoresIndexPage({ availableStores }: Props) {
const { locale } = useRouter()
const prev = usePrevPageRouter()
const router = useRouter()

return (
<>
<StoreSwitcherFormProvider
availableStores={availableStores}
onSubmit={async (data) => {
await router.push(prev?.asPath ?? '/', undefined, {
locale: storeToLocale(data.storeCode),
scroll: false,
})
}}
>
<PageMeta title={i18n._(/* i18n */ 'Switch stores')} metaRobots={['noindex']} />
<LayoutOverlayHeader>
<LayoutOverlayHeader
primary={
<StoreSwitcherLinkOrButton color='secondary' button={{ variant: 'pill' }}>
<Trans id='Switch' />
</StoreSwitcherLinkOrButton>
}
>
<LayoutTitle size='small' component='span' icon={iconLanguage}>
<Trans id='Country' />
<Trans id='Switch Stores' />
</LayoutTitle>
</LayoutOverlayHeader>
<Container maxWidth='md'>
<Container maxWidth='sm' sx={(theme) => ({ mb: theme.spacings.lg })}>
<LayoutTitle icon={iconLanguage}>
<Trans id='Country' />
<Trans id='Switch Stores' />
</LayoutTitle>
<StoreSwitcherList availableStores={availableStores} locale={locale} />
<StoreSwitcherGroupSelector
// header={<SectionHeader labelLeft='Country' />}
showStores={1}
showCurrencies={1}
/>
<StoreSwitcherStoreSelector
header={<SectionHeader labelLeft='Store' />}
showCurrencies={1}
/>
<StoreSwitcherCurrencySelector header={<SectionHeader labelLeft='Currency' />} />

<FormActions>
<StoreSwitcherApplyButton color='secondary' variant='pill' size='large'>
<Trans id='Switch' />
</StoreSwitcherApplyButton>
</FormActions>
</Container>
</>
</StoreSwitcherFormProvider>
)
}

const pageOptions: PageOptions<LayoutOverlayProps> = {
overlayGroup: 'left',
Layout: LayoutOverlay,
layoutProps: { variantMd: 'left' },
layoutProps: { variantMd: 'right', sizeMd: 'floating', justifyMd: 'start' },
}
StoresIndexPage.pageOptions = pageOptions

Expand All @@ -56,13 +95,14 @@ export default StoresIndexPage
export const getStaticProps: GetPageStaticProps = async (context) => {
const client = graphqlSharedClient(context)
const staticClient = graphqlSsrClient(context)

const conf = client.query({ query: StoreConfigDocument })
const layout = staticClient.query({ query: LayoutDocument })
const stores = staticClient.query({ query: StoreSwitcherListDocument })

return {
props: {
...(await stores).data,
...(await layout).data,
apolloState: await conf.then(() => client.cache.extract()),
},
}
Expand Down
74 changes: 59 additions & 15 deletions examples/magento-open-source/pages/switch-stores.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,109 @@
import type { PageOptions } from '@graphcommerce/framer-next-pages'
import { usePrevPageRouter, type PageOptions } from '@graphcommerce/framer-next-pages'
import type { StoreSwitcherListQuery } from '@graphcommerce/magento-store'
import {
PageMeta,
StoreConfigDocument,
StoreSwitcherList,
StoreSwitcherApplyButton,
StoreSwitcherCurrencySelector,
StoreSwitcherFormProvider,
StoreSwitcherGroupSelector,
StoreSwitcherLinkOrButton,
StoreSwitcherListDocument,
StoreSwitcherStoreSelector,
storeToLocale,
} from '@graphcommerce/magento-store'
import type { GetStaticProps } from '@graphcommerce/next-ui'
import { iconLanguage, LayoutOverlayHeader, LayoutTitle } from '@graphcommerce/next-ui'
import {
FormActions,
iconLanguage,
LayoutOverlayHeader,
LayoutTitle,
OverlayStickyBottom,
SectionHeader,
} from '@graphcommerce/next-ui'
import { i18n } from '@lingui/core'
import { Trans } from '@lingui/react'
import { Container } from '@mui/material'
import { useRouter } from 'next/router'
import type { LayoutOverlayProps } from '../components'
import { LayoutOverlay } from '../components'
import { LayoutDocument, LayoutOverlay } from '../components'
import { graphqlSharedClient, graphqlSsrClient } from '../lib/graphql/graphqlSsrClient'

type RouteProps = { country?: string[] }
type RouteProps = Record<string, unknown>
type Props = StoreSwitcherListQuery
type GetPageStaticProps = GetStaticProps<LayoutOverlayProps, Props, RouteProps>

function StoresIndexPage({ availableStores }: Props) {
const { locale } = useRouter()
const prev = usePrevPageRouter()
const router = useRouter()

return (
<>
<StoreSwitcherFormProvider
availableStores={availableStores}
onSubmit={async (data) => {
await router.push(prev?.asPath ?? '/', undefined, {
locale: storeToLocale(data.storeCode),
scroll: false,
})
}}
>
<PageMeta title={i18n._(/* i18n */ 'Switch stores')} metaRobots={['noindex']} />
<LayoutOverlayHeader>
<LayoutOverlayHeader
primary={
<StoreSwitcherLinkOrButton color='secondary' button={{ variant: 'pill' }}>
<Trans id='Switch' />
</StoreSwitcherLinkOrButton>
}
>
<LayoutTitle size='small' component='span' icon={iconLanguage}>
<Trans id='Country' />
<Trans id='Switch Stores' />
</LayoutTitle>
</LayoutOverlayHeader>
<Container maxWidth='md'>
<Container maxWidth='sm' sx={(theme) => ({ mb: theme.spacings.lg })}>
<LayoutTitle icon={iconLanguage}>
<Trans id='Country' />
<Trans id='Switch Stores' />
</LayoutTitle>
<StoreSwitcherList availableStores={availableStores} locale={locale} />
<StoreSwitcherGroupSelector
// header={<SectionHeader labelLeft='Country' />}
showStores={1}
showCurrencies={1}
/>
<StoreSwitcherStoreSelector
header={<SectionHeader labelLeft='Store' />}
showCurrencies={1}
/>
<StoreSwitcherCurrencySelector header={<SectionHeader labelLeft='Currency' />} />
<FormActions>
<StoreSwitcherApplyButton color='secondary' variant='pill' size='large'>
<Trans id='Switch' />
</StoreSwitcherApplyButton>
</FormActions>
</Container>
</>
</StoreSwitcherFormProvider>
)
}

const pageOptions: PageOptions<LayoutOverlayProps> = {
overlayGroup: 'left',
Layout: LayoutOverlay,
layoutProps: { variantMd: 'left' },
layoutProps: { variantMd: 'right', sizeMd: 'floating', justifyMd: 'start' },
}

StoresIndexPage.pageOptions = pageOptions

export default StoresIndexPage

export const getStaticProps: GetPageStaticProps = async (context) => {
const client = graphqlSharedClient(context)
const staticClient = graphqlSsrClient(context)

const conf = client.query({ query: StoreConfigDocument })
const layout = staticClient.query({ query: LayoutDocument })
const stores = staticClient.query({ query: StoreSwitcherListDocument })

return {
props: {
...(await stores).data,
...(await layout).data,
apolloState: await conf.then(() => client.cache.extract()),
},
}
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-mesh/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './.mesh'
// @ts-expect-error getBuiltMesh and createBuiltMeshHTTPHandler are re-exported here and override the export from .mesh
export * from './api/globalThisMesh'
export * from './utils/traverseSelectionSet'
export * from './utils/storefrontFromContext'
10 changes: 10 additions & 0 deletions packages/graphql-mesh/utils/storefrontFromContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { GraphCommerceStorefrontConfig } from '@graphcommerce/next-config'
import type { MeshContext } from '@graphql-mesh/runtime'

export function storefrontFromContext(
context: MeshContext & { headers?: Record<string, string> },
): GraphCommerceStorefrontConfig | undefined {
const storefrontAll = import.meta.graphCommerce.storefront
const store = context.headers?.store
return storefrontAll.find((s) => s.magentoStoreCode === store)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export const persistenceMapper = (data: string): Promise<string> => {
'ROOT_QUERY.countries',
'ROOT_QUERY.checkoutAgreements',
'ROOT_QUERY.storeConfig',
'ROOT_QUERY.currency',
'ROOT_QUERY.guestOrder',
'ROOT_QUERY.cmsBlocks',
'ROOT_QUERY.__type*',
'*Product:{"uid":"*"}.crosssell_products',
'ROOT_QUERY.recaptchaV3Config',
Expand Down
8 changes: 8 additions & 0 deletions packages/magento-store/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @graphcommerce/magento-store

## Store Switcher

![](./docs/store-switcher-single-group-multiple-stores-single-currency.png)
![](./docs/store-switcher-single-group-multiple-stores-multiple-currencies.png)
![](./docs/store-switcher-multiple-groups-one-store-single-currency.png)
![](./docs/store-switcher-multiple-groups-one-store-multiple-currency.png)
5 changes: 0 additions & 5 deletions packages/magento-store/StoreConfig.graphql

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import {
CurrencySymbol as CurrencySymbolBase,
type CurrencySymbolProps,
} from '@graphcommerce/next-ui'
import { StoreConfigDocument } from '../../StoreConfig.gql'
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'

export function CurrencySymbol(props: CurrencySymbolProps) {
const { currency } = props
const baseCurrencyCode = useQuery(StoreConfigDocument).data?.storeConfig?.base_currency_code ?? ''

return <CurrencySymbolBase {...props} currency={baseCurrencyCode} />
return <CurrencySymbolBase {...props} currency={currency || baseCurrencyCode} />
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useQuery } from '@graphcommerce/graphql'
import type { GlobalHeadProps as GlobalHeadPropsBase } from '@graphcommerce/next-ui'
import { GlobalHead as GlobalHeadBase } from '@graphcommerce/next-ui'
import { StoreConfigDocument } from '../../StoreConfig.gql'
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'

export type GlobalHeadProps = Omit<GlobalHeadPropsBase, 'name'>

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { useQuery } from '@graphcommerce/graphql'
import type { CurrencyFormatProps } from '@graphcommerce/next-ui'
import { CurrencyFormat } from '@graphcommerce/next-ui'
import type { SxProps, Theme } from '@mui/material'
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'
import type { MoneyFragment } from './Money.gql'
import { StoreConfigDocument } from './StoreConfig.gql'

type OverridableProps = {
round?: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useQuery } from '@graphcommerce/graphql'
import type { PageMetaProps as NextPageMetaProps } from '@graphcommerce/next-ui'
import { PageMeta as NextPageMeta } from '@graphcommerce/next-ui'
import { StoreConfigDocument } from './StoreConfig.gql'
import { StoreConfigDocument } from '../../queries/StoreConfig.gql'

export type PageMetaProps = Omit<NextPageMetaProps, 'canonical'> & {
canonical?: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useFormState } from '@graphcommerce/ecommerce-ui'
import type { ButtonProps, LinkOrButtonProps } from '@graphcommerce/next-ui'
import { Button, LinkOrButton } from '@graphcommerce/next-ui'
import { useStoreSwitcherForm } from './useStoreSwitcher'

export function StoreSwitcherApplyButton(props: ButtonProps<'button'>) {
const { control } = useStoreSwitcherForm()
const formState = useFormState({ control })
return <Button type='submit' loading={formState.isSubmitting} {...props} />
}

export function StoreSwitcherLinkOrButton(props: LinkOrButtonProps) {
const { control } = useStoreSwitcherForm()
const formState = useFormState({ control })
return <LinkOrButton type='submit' loading={formState.isSubmitting} {...props} />
}
Loading

0 comments on commit 4b6a65d

Please sign in to comment.