Skip to content

Commit

Permalink
dont force login
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dustinfirman committed Apr 11, 2024
1 parent b6988bd commit 994ba78
Show file tree
Hide file tree
Showing 24 changed files with 331 additions and 2,149 deletions.
44 changes: 44 additions & 0 deletions examples/b2b/README.md
@@ -0,0 +1,44 @@
# Hydrogen example: B2B

> Note:
>
> This example is currently Unstable
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
4. Using a storefront `customerAccessToken` and `companyLocationId` to [contextualize queries](https://shopify.dev/docs/api/storefront#directives) using the `buyer` argument

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/root.tsx`](app/root.tsx) | Includes a customer query to determine if the the logged in session is for a B2B buyer. Set `companyLocationId` in session if there is only one location available to buy for |
| 🆕 [`app/graphql/CustomerLocationsQuery.ts`](app/graphql/CustomerLocationsQuery.ts) | Customer query to fetch company locations |
| 🆕 [`app/components/LocationSelector.tsx`](app/components/LocationSelector.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 |
12 changes: 8 additions & 4 deletions examples/b2b/app/components/Header.tsx
Expand Up @@ -4,6 +4,10 @@ import {useState, Suspense} from 'react';
import type {HeaderQuery} from 'storefrontapi.generated';
import type {LayoutProps} from './Layout';
import {useRootLoaderData} from '~/root';
import type {
CompanyLocation,
CompanyLocationConnection,
} from '@shopify/hydrogen-react/customer-account-api-types';

/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
Expand Down Expand Up @@ -156,21 +160,21 @@ function CartToggle({cart}: Pick<HeaderProps, 'cart'>) {
/********** EXAMPLE UPDATE STARTS ************/
function LocationDropdown({company}: Pick<HeaderProps, 'company'>) {
const locations = company?.locations?.edges
? company.locations.edges.map((location) => {
? company.locations.edges.map((location: CompanyLocationConnection) => {
return {...location.node};
})
: [];

const [selectedLocation, setSelectedLocation] = useState(
company.locations.edges[0].node.id ?? undefined,
company?.locations?.edges?.[0]?.node?.id ?? undefined,
);

const setLocation = async (event: React.ChangeEvent<HTMLSelectElement>) => {
const locationId = event.target.value;
setSelectedLocation(locationId);
};

if (locations.length === 1) return null;
if (locations.length === 1 || !company) return null;

return (
<CartForm route="/cart" action={CartForm.ACTIONS.BuyerIdentityUpdate}>
Expand All @@ -187,7 +191,7 @@ function LocationDropdown({company}: Pick<HeaderProps, 'company'>) {
value={selectedLocation}
style={{marginRight: '4px'}}
>
{locations.map((location) => {
{locations.map((location: CompanyLocation) => {
return (
<option
defaultValue={selectedLocation}
Expand Down
7 changes: 4 additions & 3 deletions examples/b2b/app/components/LocationSelector.tsx
Expand Up @@ -2,12 +2,13 @@ import {CartForm} from '@shopify/hydrogen';
import type {
Company,
CompanyLocation,
CompanyLocationConnection,
} from '@shopify/hydrogen-react/customer-account-api-types';

export function LocationSelector({company}: {company: Company}) {
const locations = company?.locations?.edges
? company.locations.edges.map((loc) => {
return {...loc.node};
? company.locations.edges.map((location: CompanyLocationConnection) => {
return {...location.node};
})
: [];

Expand Down Expand Up @@ -42,7 +43,7 @@ export function LocationSelector({company}: {company: Company}) {
<CartForm route="/cart" action={CartForm.ACTIONS.BuyerIdentityUpdate}>
<fieldset>
<legend>Choose a location:</legend>
{locations.map((location) => {
{locations.map((location: CompanyLocation) => {
return (
<div key={location.id}>
<LocationItem location={location} />
Expand Down
58 changes: 39 additions & 19 deletions examples/b2b/app/root.tsx
Expand Up @@ -70,11 +70,15 @@ export const useRootLoaderData = () => {
return root?.data as SerializeFrom<typeof loader>;
};

export async function loader({request, context}: LoaderFunctionArgs) {
const {storefront, customerAccount, cart, session} = context;
export async function loader({context}: LoaderFunctionArgs) {
const {storefront, customerAccount, cart} = context;
const publicStoreDomain = context.env.PUBLIC_STORE_DOMAIN;

/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
const isLoggedIn = await customerAccount.isLoggedIn();
/********** EXAMPLE UPDATE END ************/
/***********************************************/
const cartPromise = cart.get();

// defer the footer query (below the fold)
Expand All @@ -93,35 +97,46 @@ export async function loader({request, context}: LoaderFunctionArgs) {
},
});

/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
// B2B buyer context
let companyLocationId = (await customerAccount.UNSTABLE_getBuyer())
?.companyLocationId;
let companyLocationId;
let company: Company;

const customer = await customerAccount.query(CUSTOMER_LOCATIONS_QUERY);
const company: Company =
customer?.data?.customer?.companyContacts?.edges?.[0]?.node?.company;
if (isLoggedIn) {
companyLocationId = (await customerAccount.UNSTABLE_getBuyer())
?.companyLocationId;

const customer = await customerAccount.query(CUSTOMER_LOCATIONS_QUERY);
company =
customer?.data?.customer?.companyContacts?.edges?.[0]?.node?.company;
}

if (!companyLocationId && company?.locations?.edges?.length === 1) {
companyLocationId = company.locations.edges[0].node.id;

customerAccount.UNSTABLE_setBuyer({
companyLocationId,
});

//updateBuyerIdentity
}

const showLocationSelector = Boolean(company && !companyLocationId);
/********** EXAMPLE UPDATE END ************/
/***********************************************/

return defer(
{
cart: cartPromise,
footer: footerPromise,
header: await headerPromise,
/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
isLoggedIn,
publicStoreDomain,
company,
showLocationSelector,
/********** EXAMPLE UPDATE END ************/
/***********************************************/
},
{
headers: {
Expand All @@ -144,16 +159,21 @@ export default function App() {
<Links />
</head>
<body>
{data.showLocationSelector ? (
<main>
<LocationSelector company={data.company} />
</main>
) : (
<Layout {...data}>
<Outlet />
</Layout>
)}

{
/***********************************************/
/********** EXAMPLE UPDATE STARTS ************/
data.showLocationSelector ? (
<main>
<LocationSelector company={data.company} />
</main>
) : (
<Layout {...data}>
<Outlet />
</Layout>
)
/********** EXAMPLE UPDATE END ************/
/***********************************************/
}
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
Expand Down
145 changes: 0 additions & 145 deletions examples/b2b/app/routes/_index.tsx

This file was deleted.

0 comments on commit 994ba78

Please sign in to comment.