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

Contribution guidelines for examples, with an example example #1427

Merged
merged 4 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Contributing Hydrogen examples
juanpprieto marked this conversation as resolved.
Show resolved Hide resolved

Hydrogen is an open source project, and we welcome your contributions! Sharing examples is a great way to showcase Hydrogen's full range of capabilities. Examples are also the best way to demonstrate third-party integrations that aren’t a natural fit for Shopify’s official documentation.

## Project structure

An example project should provide the minimal functionality necessary to illustrate the concept.

### Baseline functionality

- Always use Hydrogen's [Skeleton template](/templates/skeleton) as the baseline for examples.
- Only include the files that are required to illustrate the example.
- For instance, if your example requires editing the product detail page, keep the `app/routes/products.$handle.tsx` file to show your updates, but delete all other route files.
- The goal is to maintain focus on the relevant example code, and reduce the burden of maintaining examples.

### Example folder naming

- Give the example a descriptive name.
- Pick key words that someone is likely to search for in the page with `ctrl-F`.
- If similar examples already exist, pick a similar naming pattern so they group alphabetically.
- Use `kebab-case`.
- Use lowercase letters only.

### Language

- Default to TypeScript
- Types provide a measure of self-documentation for examples, cutting down on verbosity and redundant code comments.
- It’s easier to remove or omit unneeded types than to add them in later.

### Comments

- Use code comments strategically throughout the example. Less is more.
- Use [JSDoc](https://jsdoc.app/) syntax to document functions.
- Opt for more descriptive function and variable names to cut down on redundant comments explaining what they do.
- Use comments to highlight common mistakes, resolve ambiguity, or explain non-obvious context.

## README

Every example needs a README file that explains what it does and how it works. Each README should follow this guideline and include the following sections:

### Introduction

- Provide a few sentences that explain what the example illustrates.
- Keep it short, descriptive, and factual.

### Requirements

- Provide a point-form list of anything you’ll need before you start.
- Examples: account logins, third-party API tokens, feature access, beta flags, etc.
- If the example integrates a third-party service, link to the relevant docs.
- The goal isn't to document that other platform; select links that focus on completing the task at hand.

### Key files

A table listing the relevant files makes it easier to quickly scan the example and understand its complexity.

- Provide a table with the list of files the user will need to create or edit.
- Start with new files that you need to create, then files that require editing.
- Prefix newly created files with the 🆕 emoji.
- Link the file name to the actual file in the example codebase.
- Add a brief description of the file's purpose.
- If the example requires environment variables, document them in a `.env.example` file.

### Instructions

- In general, use the file list above as the order of operations.
1. Handle creating new files first.
1. Then describe updates and edits to existing Hydrogen default files.
- Ideally, structure the instructions so the user touches each file once, instead of returning to files multiple times across different steps. This way, the list of files serves as both a table of contents and a TODO list for the developer.
9 changes: 9 additions & 0 deletions examples/multipass/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
PUBLIC_STORE_DOMAIN="example.myshopify.com"

# Note: Multipass is only available on Shopify Plus plans.
# https://www.shopify.com/admin/settings/customer_accounts
PRIVATE_SHOPIFY_STORE_MULTIPASS_SECRET="multipass_secret_token"

# If you don’t use a custom checkout domain, use the same
# value as PUBLIC_STORE_DOMAIN above
PRIVATE_SHOPIFY_CHECKOUT_DOMAIN="checkout.example.com"
143 changes: 143 additions & 0 deletions examples/multipass/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# skeleton

## 1.0.0

### Major Changes

- The Storefront API 2023-10 now returns menu item URLs that include the `primaryDomainUrl`, instead of defaulting to the Shopify store ID URL (example.myshopify.com). The skeleton template requires changes to check for the `primaryDomainUrl`: by [@blittle](https://github.com/blittle)

1. Update the `HeaderMenu` component to accept a `primaryDomainUrl` and include
it in the internal url check

```diff
// app/components/Header.tsx

+ import type {HeaderQuery} from 'storefrontapi.generated';

export function HeaderMenu({
menu,
+ primaryDomainUrl,
viewport,
}: {
menu: HeaderProps['header']['menu'];
+ primaryDomainUrl: HeaderQuery['shop']['primaryDomain']['url'];
viewport: Viewport;
}) {

// ...code

// if the url is internal, we strip the domain
const url =
item.url.includes('myshopify.com') ||
item.url.includes(publicStoreDomain) ||
+ item.url.includes(primaryDomainUrl)
? new URL(item.url).pathname
: item.url;

// ...code

}
```

2. Update the `FooterMenu` component to accept a `primaryDomainUrl` prop and include
it in the internal url check

```diff
// app/components/Footer.tsx

- import type {FooterQuery} from 'storefrontapi.generated';
+ import type {FooterQuery, HeaderQuery} from 'storefrontapi.generated';

function FooterMenu({
menu,
+ primaryDomainUrl,
}: {
menu: FooterQuery['menu'];
+ primaryDomainUrl: HeaderQuery['shop']['primaryDomain']['url'];
}) {
// code...

// if the url is internal, we strip the domain
const url =
item.url.includes('myshopify.com') ||
item.url.includes(publicStoreDomain) ||
+ item.url.includes(primaryDomainUrl)
? new URL(item.url).pathname
: item.url;

// ...code

);
}
```

3. Update the `Footer` component to accept a `shop` prop

```diff
export function Footer({
menu,
+ shop,
}: FooterQuery & {shop: HeaderQuery['shop']}) {
return (
<footer className="footer">
- <FooterMenu menu={menu} />
+ <FooterMenu menu={menu} primaryDomainUrl={shop.primaryDomain.url} />
</footer>
);
}
```

4. Update `Layout.tsx` to pass the `shop` prop

```diff
export function Layout({
cart,
children = null,
footer,
header,
isLoggedIn,
}: LayoutProps) {
return (
<>
<CartAside cart={cart} />
<SearchAside />
<MobileMenuAside menu={header.menu} shop={header.shop} />
<Header header={header} cart={cart} isLoggedIn={isLoggedIn} />
<main>{children}</main>
<Suspense>
<Await resolve={footer}>
- {(footer) => <Footer menu={footer.menu} />}
+ {(footer) => <Footer menu={footer.menu} shop={header.shop} />}
</Await>
</Suspense>
</>
);
}
```

### Patch Changes

- If you are calling `useMatches()` in different places of your app to access the data returned by the root loader, you may want to update it to the following pattern to enhance types: ([#1289](https://github.com/Shopify/hydrogen/pull/1289)) by [@frandiox](https://github.com/frandiox)

```ts
// root.tsx

import {useMatches} from '@remix-run/react';
import {type SerializeFrom} from '@shopify/remix-oxygen';

export const useRootLoaderData = () => {
const [root] = useMatches();
return root?.data as SerializeFrom<typeof loader>;
};

export function loader(context) {
// ...
}
```

This way, you can import `useRootLoaderData()` anywhere in your app and get the correct type for the data returned by the root loader.

- Updated dependencies [[`81400439`](https://github.com/Shopify/hydrogen/commit/814004397c1d17ef0a53a425ed28a42cf67765cf), [`a6f397b6`](https://github.com/Shopify/hydrogen/commit/a6f397b64dc6a0d856cb7961731ee1f86bf80292), [`3464ec04`](https://github.com/Shopify/hydrogen/commit/3464ec04a084e1ceb30ee19874dc1b9171ce2b34), [`7fc088e2`](https://github.com/Shopify/hydrogen/commit/7fc088e21bea47840788cb7c60f873ce1f253128), [`867e0b03`](https://github.com/Shopify/hydrogen/commit/867e0b033fc9eb04b7250baea97d8fd49d26ccca), [`ad45656c`](https://github.com/Shopify/hydrogen/commit/ad45656c5f663cc1a60eab5daab4da1dfd0e6cc3), [`f24e3424`](https://github.com/Shopify/hydrogen/commit/f24e3424c8e2b363b181b71fcbd3e45f696fdd3f), [`66a48573`](https://github.com/Shopify/hydrogen/commit/66a4857387148b6a104df5783314c74aca8aada0), [`0ae7cbe2`](https://github.com/Shopify/hydrogen/commit/0ae7cbe280d8351126e11dc13f35d7277d9b2d86), [`8198c1be`](https://github.com/Shopify/hydrogen/commit/8198c1befdfafb39fbcc88d71f91d21eae252973), [`ad45656c`](https://github.com/Shopify/hydrogen/commit/ad45656c5f663cc1a60eab5daab4da1dfd0e6cc3)]:
- @shopify/[email protected]
- @shopify/[email protected]
- @shopify/[email protected]
81 changes: 81 additions & 0 deletions examples/multipass/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Hydrogen example: Multipass

This folder contains an example implementation of [Multipass](https://shopify.dev/docs/api/multipass) for Hydrogen. It shows how to persist
the user session from a Hydrogen storefront through to checkout.

## Requirements

- Multipass is available on [Shopify Plus](https://www.shopify.com/plus) plans.
- A Shopify Multipass secret token. Go to [**Settings > Customer accounts**](https://www.shopify.com/admin/settings/customer_accounts) to create one.

## Key files

This folder contains the minimal set of files needed to showcase the implementation.
Files that aren’t included by default with Hydrogen and that you’ll need to
create are labeled with 🆕.

| File | Description |
| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| 🆕 [`.env.example`](.env.example) | Example environment variable file. Copy the relevant variables to your existing `.env` file, if you have one. |
| 🆕 [`app/components/MultipassCheckoutButton.tsx`](app/components/MultipassCheckoutButton.tsx) | Checkout button component that passes the customer session to checkout. |
| 🆕 [`app/utils/multipass/multipass.ts`](app/utils/multipass/multipass.ts) | Utility function that handles getting a multipass URL and token. |
| 🆕 [`app/utils/multipass/multipassify.server.ts`](app/lib/multipass/multipassify.server.ts) | Utility that handles creating and parse multipass tokens. |
| 🆕 [`app/utils/multipass/types.ts`](app/utils/multipass/types.ts) | Types for multipass utilities. |
| 🆕 [`app/routes/($lang).account._public.login.multipass.tsx`](<app/routes/($lang).account._public.login.multipass.tsx>) | API route that returns generated multipass tokens. |
| [`app/components/Cart.tsx`](app/components/Cart.tsx) | Hydrogen cart component, which gets updated to add the `<MultipassCheckoutButton>` component. |

## Dependencies

| Module | Description |
| ----------------------------------------------------------------------- | --------------------------------------- |
| 🆕 [`snakecase-keys`](https://www.npmjs.com/package/snakecase-keys) | Convert an object's keys to snake case |
| 🆕 [`crypto-js`](https://www.npmjs.com/package/crypto-js) | JavaScript library of crypto standards. |
| 🆕 [`@types/crypto-js`](https://www.npmjs.com/package/@types/crypto-js) | crypto-js TypeScript types |

## Instructions

### 1. Install required dependencies

```bash
# JavaScript
npm i @snakecase-keys crypto-js

# TypeScript
npm i @snakecase-keys crypto-js
npm i --save-dev @types/crypto-js
```

### 2. Copy over the new files

- In your Hydrogen app, create the new files from the file list above, copying in the code as you go.
- If you already have a `.env` file, copy over these key-value pairs:
- `PRIVATE_SHOPIFY_STORE_MULTIPASS_SECRET`
- `PRIVATE_SHOPIFY_CHECKOUT_DOMAIN`

### 3. Edit the Cart component file

Import `MultipassCheckoutButton` and update the `CartCheckoutActions()` function. Wrap the standard `<Button>` component with the `<MultiPassCheckoutButton>` component:

```tsx
// app/components/Cart.tsx

import {MultipassCheckoutButton} from '~/components';

// ...

function CartCheckoutActions({checkoutUrl}: {checkoutUrl: string}) {
if (!checkoutUrl) return null;

return (
<div>
<MultipassCheckoutButton checkoutUrl={checkoutUrl}>
<Button>Continue to Checkout</Button>
</MultipassCheckoutButton>
</div>
);
}

// ...
```

[View the complete component file](app/components/Cart.tsx) to see these updates in context.
47 changes: 47 additions & 0 deletions examples/multipass/app/components/Aside.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* A side bar component with Overlay that works without JavaScript.
* @example
* ```jsx
* <Aside id="search-aside" heading="SEARCH">
* <input type="search" />
* ...
* </Aside>
* ```
*/
export function Aside({
children,
heading,
id = 'aside',
}: {
children?: React.ReactNode;
heading: React.ReactNode;
id?: string;
}) {
return (
<div aria-modal className="overlay" id={id} role="dialog">
<button
className="close-outside"
onClick={() => {
history.go(-1);
window.location.hash = '';
}}
/>
<aside>
<header>
<h3>{heading}</h3>
<CloseAside />
</header>
<main>{children}</main>
</aside>
</div>
);
}

function CloseAside() {
return (
/* eslint-disable-next-line jsx-a11y/anchor-is-valid */
<a className="close" href="#" onChange={() => history.go(-1)}>
&times;
</a>
);
}
Loading
Loading