Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* b2b example start working queries contextualized queries quantity and volume prices added header selector login redirect handle single location buyers convert location selector to form convert location dropdown to form update input type * remove getBuyer and move it into context automatically move token exchange and add get set buyer move set buyer into customerAccount remove buyer expire remove locations and use cart instead automatically get buyeridentity from customerAccount * add type, dont merge this thou, its based on unstable * dont force login added example comments removed comment added README type updates fixed tests added more tests updated types to 2024-04 simplified example doc update updated props to unstable * fixes after rebase * regenerate graphql types * fixing types * more type fixes * add dts file * fix more types * add generated types * cart handles quantity rules * catch error on cart * check for b2b customer using a provider * fixed types * cart update * convert location selector to list of forms * decouple caapi from storefront * finished addressing feedback * fixed types and linting * removed unused test * readme update * addressed comments * Clean up cart changes * clean up buyer identity update * more clean up * removed unused imports * update type import * fixed typecheck * reconstruct buyer * add example fix for CLI * sync server file with skeleton more closetly * addressed comments * update cart on logout and changeset * added redirect issue doc * updated changeset summary --------- Co-authored-by: Michelle Chen <michelle.chen@shopify.com> Co-authored-by: Helen Lin <helen.lin@shopify.com>
- Loading branch information
1 parent
a9c6de0
commit ae262b6
Showing
41 changed files
with
4,822 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@shopify/hydrogen': patch | ||
--- | ||
|
||
Adding support for B2B to the customer account client and cart handler to store and manage [buyer context](https://shopify.dev/docs/api/storefront/2024-04/input-objects/BuyerInput). Currently Unstable. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.shopify |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Hydrogen example: B2B | ||
|
||
> [!NOTE] | ||
> This example is currently Unstable. There is a known issue where setting too many [Customer Account API callback URIs](https://shopify.dev/docs/custom-storefronts/building-with-the-customer-account-api/hydrogen#update-the-application-setup) will cause the hydrogen session to exceed the browsers maximum cookie length. This is because our current implementation relies on encoding redirect URIs in the token. We are aware of this issue and are actively working towards a future where this is not a problem. As a workaround you can remove unneeded callback URIs or use a different storefront. | ||
This is an example implementation of a B2B storefront using Hydrogen. It includes the following high level changes. | ||
|
||
1. Retrieving company location data from a logged in customer using the [Customer Account API](https://shopify.dev/docs/api/customer/2024-04/queries/customer) | ||
2. Displaying a list of company locations and setting a `companyLocationId` in session | ||
3. Using a storefront `customerAccessToken` and `companyLocationId` to update cart and get B2B specific rules and pricing such as [volume pricing and quantity rules](https://help.shopify.com/en/manual/b2b/catalogs/quantity-pricing) | ||
4. Using a storefront `customerAccessToken` and `companyLocationId` to [contextualize queries](https://shopify.dev/docs/api/storefront#directives) using the `buyer` argument on the product display page | ||
|
||
> [!NOTE] | ||
> Only queries on the product display page, `app/routes/products.$handle.tsx`, were contextualized in this example. For a production storefront, all queries for product data should be contextualized. | ||
## Install | ||
|
||
Setup a new project with this example: | ||
|
||
```bash | ||
npm create @shopify/hydrogen@latest -- --template b2b | ||
``` | ||
|
||
## Requirements | ||
|
||
- Your store is on a [Shopify Plus plan](https://help.shopify.com/manual/intro-to-shopify/pricing-plans/plans-features/shopify-plus-plan). | ||
- Your store is using [new customer accounts](https://help.shopify.com/en/manual/customers/customer-accounts/new-customer-accounts). | ||
- You have access to a customer which has permission to order for a [B2B company](https://help.shopify.com/en/manual/b2b). | ||
|
||
## Key files | ||
|
||
This folder contains the minimal set of files needed to showcase the implementation. | ||
Not all queries where contextualized for B2B. `app/routes/products.$handle.tsx` provides | ||
reference on how to contextualize storefront queries. Files that aren’t included by default | ||
with Hydrogen and that you’ll need to create are labeled with 🆕. | ||
|
||
| File | Description | | ||
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | ||
| [`app/routes/b2blocations.tsx`](app/routes/b2blocations.tsx) | Includes a customer query to get B2B data. Set `companyLocationId` in session if there is only one location available to buy for the customer | | ||
| [`app/components/B2BLocationProvider.tsx`](app/components/B2BLocationProvider.tsx) | Provides context on if the current logged in customer is a B2B customer and keeping track of the location modal open status. | | ||
| 🆕 [`app/graphql/CustomerLocationsQuery.ts`](app/graphql/CustomerLocationsQuery.ts) | Customer query to fetch company locations | | ||
| 🆕 [`app/components/B2BLocationSelector.tsx`](app/components/B2BLocationSelector.tsx) | Component to choose a Company location to buy for. Rendered if there is no `companyLocationId` set in session | | ||
| [`app/routes/products.$handle.tsx`](app/routes/products.$handle.tsx) | Added buyer context to the product and product varient queries. Includes logic and components to display quantity rules and quantity price breaks | | ||
| 🆕 [`app/components/PriceBreaks.tsx`](app/components/PriceBreaks.tsx) | Component rendered on the product page to highlight quantity price breaks | | ||
| 🆕 [`app/components/QuantityRules.tsx`](app/components/QuantityRules.tsx) | Component rendered on the product page to highlight quantity rules | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import {createContext, useContext, useEffect, useState, useMemo} from 'react'; | ||
import {useFetcher} from '@remix-run/react'; | ||
import {CustomerCompany} from '../root'; | ||
|
||
export type B2BLocationContextValue = { | ||
company?: CustomerCompany; | ||
companyLocationId?: string; | ||
modalOpen?: boolean; | ||
setModalOpen: (b: boolean) => void; | ||
}; | ||
|
||
const defaultB2BLocationContextValue = { | ||
company: undefined, | ||
companyLocationId: undefined, | ||
modalOpen: undefined, | ||
setModalOpen: () => {}, | ||
}; | ||
|
||
const B2BLocationContext = createContext<B2BLocationContextValue>( | ||
defaultB2BLocationContextValue, | ||
); | ||
|
||
export function B2BLocationProvider({children}: {children: React.ReactNode}) { | ||
const fetcher = useFetcher<B2BLocationContextValue>(); | ||
const [modalOpen, setModalOpen] = useState(fetcher?.data?.modalOpen); | ||
|
||
useEffect(() => { | ||
if (fetcher.data || fetcher.state === 'loading') return; | ||
|
||
fetcher.load('/b2blocations'); | ||
}, [fetcher]); | ||
|
||
const value = useMemo<B2BLocationContextValue>(() => { | ||
return { | ||
...defaultB2BLocationContextValue, | ||
...fetcher.data, | ||
modalOpen: modalOpen ?? fetcher?.data?.modalOpen, | ||
setModalOpen, | ||
}; | ||
}, [fetcher, modalOpen]); | ||
|
||
return ( | ||
<B2BLocationContext.Provider value={value}> | ||
{children} | ||
</B2BLocationContext.Provider> | ||
); | ||
} | ||
|
||
export function useB2BLocation(): B2BLocationContextValue { | ||
return useContext(B2BLocationContext); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import {CartForm} from '@shopify/hydrogen'; | ||
import type { | ||
CustomerCompanyLocation, | ||
CustomerCompanyLocationConnection, | ||
} from '~/root'; | ||
import {useB2BLocation} from './B2BLocationProvider'; | ||
|
||
export function B2BLocationSelector() { | ||
const {company, modalOpen, setModalOpen} = useB2BLocation(); | ||
|
||
const locations = company?.locations?.edges | ||
? company.locations.edges.map( | ||
(location: CustomerCompanyLocationConnection) => { | ||
return {...location.node}; | ||
}, | ||
) | ||
: []; | ||
|
||
if (!company || !modalOpen) return null; | ||
|
||
return ( | ||
<div className="modal"> | ||
<div className="modal-content"> | ||
<h2>Logged in for {company.name}</h2> | ||
<legend>Choose a location:</legend> | ||
<div className="location-list"> | ||
{locations.map((location: CustomerCompanyLocation) => { | ||
const addressLines = | ||
location?.shippingAddress?.formattedAddress ?? []; | ||
return ( | ||
<CartForm | ||
key={location.id} | ||
route="/cart" | ||
action={CartForm.ACTIONS.BuyerIdentityUpdate} | ||
inputs={{ | ||
buyerIdentity: {companyLocationId: location.id}, | ||
}} | ||
> | ||
{(fetcher) => ( | ||
<label> | ||
<button | ||
onClick={(event) => { | ||
setModalOpen(false); | ||
fetcher.submit(event.currentTarget.form, { | ||
method: 'POST', | ||
}); | ||
}} | ||
className="location-item" | ||
> | ||
<div> | ||
<p> | ||
<strong>{location.name}</strong> | ||
</p> | ||
{addressLines.map((line: string) => ( | ||
<p key={line}>{line}</p> | ||
))} | ||
</div> | ||
</button> | ||
</label> | ||
)} | ||
</CartForm> | ||
); | ||
})} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.