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

Add a customer account api example #1126

Merged
merged 4 commits into from
Jul 25, 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
5 changes: 5 additions & 0 deletions .changeset/rich-ants-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/hydrogen': patch
---

Add an example using the new [Customer Account API](https://shopify.dev/docs/api/customer)
38 changes: 38 additions & 0 deletions examples/customer-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Hydrogen Customer API Example

**Caution: The Customer API and this example are both in an unstable pre-release state and may have breaking changes in a future release.**

This is an example using the new Shopify [Customer Accounts API](https://shopify.dev/docs/api/customer). Note this functionality is in dev preview, the API is subject to change, and should not be used in production.

## Requirements

1. Hydrogen 2023.7 or later
2. [Ngrok](https://ngrok.com/) for pointing a public https domain to your local machine required for oAuth

## Environment variables

Create a `.env` file at the root of your project

```toml
PUBLIC_CUSTOMER_ACCOUNT_ID=shp_<your-id>
PUBLIC_CUSTOMER_ACCOUNT_URL=https://shopify.com/<your-url-id>
PUBLIC_STOREFRONT_API_TOKEN=<your-storefront-api-token>
PUBLIC_STORE_DOMAIN=<your-store>.myshopify.com
SESSION_SECRET=foobar
```

## Setup

1. Setup a [ngrok](https://ngrok.com/) account and add a permanent domain.
2. Add the `Hydrogen` or `Headless` app/channel to your store via the Shopify admin
3. Create a storefront if one doesn't exist
4. Access the `Customer Account API` settings via the storefront settings page
5. Copy the permanent domain from ngrok and add it as a `callback URI`: `https://your-ngrok-domain.app/authorize`
6. Add a `JavaScript origin` with your ngrok domain: `https://your-ngrok-domain.app`
7. Add a logout URI to your ngrok domain: `https://your-ngrok-domain.app`
8. Copy the `Client ID` from the Customer Account API credentials to the `.env` `PUBLIC_CUSTOMER_ACCOUNT_ID` variable
9. Copy the Customer Account API url to the `.env` `PUBLIC_CUSTOMER_ACCOUNT_URL` variable
10. Update the ngrok npm script within `package.json` to use your ngrok domain
11. Install the [ngrok CLI](https://ngrok.com/download)
12. In a terminal start ngrok with `npm run ngrok`
13. In another terminal, start hydrogen with `npm run dev`
12 changes: 12 additions & 0 deletions examples/customer-api/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {RemixBrowser} from '@remix-run/react';
import {startTransition, StrictMode} from 'react';
import {hydrateRoot} from 'react-dom/client';

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
33 changes: 33 additions & 0 deletions examples/customer-api/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type {EntryContext} from '@shopify/remix-oxygen';
import {RemixServer} from '@remix-run/react';
import isbot from 'isbot';
import {renderToReadableStream} from 'react-dom/server';

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const body = await renderToReadableStream(
<RemixServer context={remixContext} url={request.url} />,
{
signal: request.signal,
onError(error) {
// eslint-disable-next-line no-console
console.error(error);
responseStatusCode = 500;
},
},
);

if (isbot(request.headers.get('user-agent'))) {
await body.allReady;
}

responseHeaders.set('Content-Type', 'text/html');
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
67 changes: 67 additions & 0 deletions examples/customer-api/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {type LinksFunction, type LoaderArgs} from '@shopify/remix-oxygen';
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from '@remix-run/react';
import type {Shop} from '@shopify/hydrogen/storefront-api-types';
import styles from './styles/app.css';
import favicon from '../public/favicon.svg';

export const links: LinksFunction = () => {
return [
{rel: 'stylesheet', href: styles},
{
rel: 'preconnect',
href: 'https://cdn.shopify.com',
},
{
rel: 'preconnect',
href: 'https://shop.app',
},
{rel: 'icon', type: 'image/svg+xml', href: favicon},
];
};

export async function loader({context}: LoaderArgs) {
const layout = await context.storefront.query<{shop: Shop}>(LAYOUT_QUERY);
return {layout};
}

export default function App() {
const data = useLoaderData<typeof loader>();

const {name} = data.layout.shop;

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<h1 style={{marginBottom: 14}}>Customer API Example</h1>
<p style={{marginBottom: 14}}>
This is an example of Hydrogen using the Customer API
</p>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

const LAYOUT_QUERY = `#graphql
query layout {
shop {
name
description
}
}
`;
65 changes: 65 additions & 0 deletions examples/customer-api/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {Form, useLoaderData, useRouteError} from '@remix-run/react';
import {type LoaderArgs, json} from '@shopify/remix-oxygen';

export async function loader({context}: LoaderArgs) {
if (await context.customer.isLoggedIn()) {
const user = await context.customer.query(`
{
personalAccount {
firstName
lastName
}
}
`);

return json({
user,
});
}
return json({user: null});
}

export function ErrorBoundary() {
const error = useRouteError() as Error;
return (
<>
<h2>
<b>Error loading the user:</b>
</h2>
<p>{error.message}</p>

<Form method="post" action="/logout" style={{marginTop: 24}}>
<button>Logout</button>
</Form>
</>
);
}

export default function () {
const {user} = useLoaderData();

return (
<div style={{marginTop: 24}}>
{user ? (
<>
<div style={{marginBottom: 24}}>
<b>
Welcome {user.personalAccount.firstName}{' '}
{user.personalAccount.lastName}
</b>
</div>
<div>
<Form method="post" action="/logout">
<button>Logout</button>
</Form>
</div>
</>
) : null}
{!user ? (
<Form method="post" action="/authorize">
<button>Login</button>
</Form>
) : null}
</div>
);
}
9 changes: 9 additions & 0 deletions examples/customer-api/app/routes/authorize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {ActionArgs, LoaderArgs} from '@shopify/remix-oxygen';

export async function action({context}: ActionArgs) {
return context.customer.login();
}

export async function loader({context}: LoaderArgs) {
return context.customer.authorize('/');
}
5 changes: 5 additions & 0 deletions examples/customer-api/app/routes/logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {ActionArgs} from '@shopify/remix-oxygen';

export async function action({context}: ActionArgs) {
return context.customer.logout();
}
31 changes: 31 additions & 0 deletions examples/customer-api/app/styles/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
body {
margin: 0;
background: #ffffff;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
padding: 20px;
}

h1,
h2,
p {
margin: 0;
padding: 0;
}

h1 {
font-size: 3rem;
font-weight: 700;
line-height: 1.4;
}

h2 {
font-size: 1.2rem;
font-weight: 700;
line-height: 1.4;
}

p {
font-size: 1rem;
line-height: 1.4;
}
Loading