Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding an example for B2B #1886

Merged
merged 36 commits into from May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cf4f945
b2b example start
dustinfirman Mar 14, 2024
223b50c
remove getBuyer and move it into context automatically
michenly Mar 25, 2024
9727ecd
add type, dont merge this thou, its based on unstable
michenly Mar 28, 2024
ddd13ce
dont force login
dustinfirman Mar 28, 2024
063d732
fixes after rebase
dustinfirman Apr 11, 2024
864d984
regenerate graphql types
wizardlyhel Apr 12, 2024
fc76bc3
fixing types
wizardlyhel Apr 12, 2024
10d1c55
more type fixes
wizardlyhel Apr 12, 2024
4bb5041
add dts file
wizardlyhel Apr 12, 2024
d28501c
fix more types
wizardlyhel Apr 12, 2024
e958527
add generated types
wizardlyhel Apr 12, 2024
865b46c
cart handles quantity rules
dustinfirman Apr 22, 2024
c5120fe
catch error on cart
dustinfirman Apr 22, 2024
b7334e2
check for b2b customer using a provider
dustinfirman Apr 24, 2024
609f8bd
fixed types
dustinfirman Apr 24, 2024
0d2e21c
cart update
dustinfirman May 1, 2024
ca3c9c8
convert location selector to list of forms
dustinfirman May 1, 2024
6f5dde2
decouple caapi from storefront
dustinfirman May 1, 2024
b442e58
finished addressing feedback
dustinfirman May 3, 2024
97c84eb
fixed types and linting
dustinfirman May 3, 2024
fb5f025
removed unused test
dustinfirman May 3, 2024
2c430d4
readme update
dustinfirman May 3, 2024
b67e01f
addressed comments
dustinfirman May 6, 2024
64cabfd
Clean up cart changes
wizardlyhel May 3, 2024
8df3481
clean up buyer identity update
wizardlyhel May 3, 2024
8447827
more clean up
wizardlyhel May 3, 2024
f033ea9
removed unused imports
dustinfirman May 6, 2024
9c780e5
update type import
dustinfirman May 6, 2024
06eab45
fixed typecheck
dustinfirman May 6, 2024
8a832c0
reconstruct buyer
dustinfirman May 6, 2024
c454408
add example fix for CLI
michenly May 6, 2024
b190436
sync server file with skeleton more closetly
michenly May 7, 2024
4028ca9
addressed comments
dustinfirman May 7, 2024
9e4add7
update cart on logout and changeset
dustinfirman May 7, 2024
8e47a77
added redirect issue doc
dustinfirman May 7, 2024
4460529
updated changeset summary
dustinfirman May 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-rivers-nail.md
@@ -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.
2 changes: 2 additions & 0 deletions .graphqlrc.yml
Expand Up @@ -8,11 +8,13 @@ projects:
- 'examples/**/app/**/*.{graphql,js,ts,jsx,tsx}'
- '!templates/**/app/graphql/**/*.{graphql,js,ts,jsx,tsx}'
- '!examples/**/app/graphql/**/*.{graphql,js,ts,jsx,tsx}'
- '!packages/hydrogen/src/customer/**/*.{graphql,js,ts,jsx,tsx}'
customer-account:
schema: 'packages/hydrogen-react/customer-account.schema.json'
documents:
- 'templates/**/app/graphql/customer-account/**/*.{graphql,js,ts,jsx,tsx}'
- 'examples/**/app/graphql/customer-account/**/*.{graphql,js,ts,jsx,tsx}'
- 'packages/hydrogen/src/customer/**/*.{graphql,js,ts,jsx,tsx}'
admin:
schema: 'packages/cli/admin.schema.json'
documents: 'packages/cli/src/**/graphql/admin/**/*.ts'
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Expand Up @@ -16,6 +16,7 @@ These are some of the most commonly used Hydrogen examples. Browse the folders i

| Example | Details |
| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [B2B](/examples/b2b/) | Hydrogen example of a headless B2B store front |
| [Custom Cart Method](/examples/custom-cart-method/) | How to implementation custom cart method by showing in-line product option edit in cart. |
| [Express](/examples/express/) | Hydrogen example using NodeJS [Express](https://expressjs.com/). |
| [Infinite Scroll](/examples/infinite-scroll/) | [Infinite scroll](https://shopify.dev/docs/custom-storefronts/hydrogen/data-fetching/pagination#automatically-load-pages-on-scroll) within a product collection page using the [Pagination component](https://shopify.dev/docs/api/hydrogen/2024-01/components/pagination). |
Expand Down
1 change: 1 addition & 0 deletions examples/b2b/.gitignore
@@ -0,0 +1 @@
.shopify
45 changes: 45 additions & 0 deletions examples/b2b/README.md
@@ -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.

dustinfirman marked this conversation as resolved.
Show resolved Hide resolved
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 |
51 changes: 51 additions & 0 deletions examples/b2b/app/components/B2BLocationProvider.tsx
@@ -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);
}
68 changes: 68 additions & 0 deletions examples/b2b/app/components/B2BLocationSelector.tsx
@@ -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>
);
}