Skip to content

Commit dbf0457

Browse files
committed
feat: working pwa example
1 parent 1e587b8 commit dbf0457

File tree

8 files changed

+171
-33
lines changed

8 files changed

+171
-33
lines changed

examples/pwa-authentication/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"@epcc-sdk/sdks-shopper": "^0.0.28",
1314
"react": "^19.1.0",
1415
"react-dom": "^19.1.0"
1516
},

examples/pwa-authentication/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/pwa-authentication/src/App.tsx

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,76 @@
1-
import { useState } from 'react'
2-
import reactLogo from './assets/react.svg'
3-
import viteLogo from '/vite.svg'
4-
import './App.css'
1+
import { useEffect, useState } from "react"
2+
import "./App.css"
3+
import {
4+
getByContextAllProducts,
5+
ProductListData,
6+
} from "@epcc-sdk/sdks-shopper"
57

68
function App() {
7-
const [count, setCount] = useState(0)
9+
const [products, setProducts] = useState<ProductListData["data"]>([])
10+
const [isAuthenticated, setIsAuthenticated] = useState(false)
11+
12+
const fetchProducts = async () => {
13+
try {
14+
const response = await getByContextAllProducts()
15+
setProducts(response.data?.data || [])
16+
if (response.data?.data && response.data?.data.length > 0) {
17+
setIsAuthenticated(true)
18+
}
19+
} catch (error) {
20+
console.error("Failed to fetch products:", error)
21+
setIsAuthenticated(false)
22+
}
23+
}
24+
25+
useEffect(() => {
26+
fetchProducts()
27+
}, [])
828

929
return (
1030
<>
11-
<div>
12-
<a href="https://vite.dev" target="_blank">
13-
<img src={viteLogo} className="logo" alt="Vite logo" />
14-
</a>
15-
<a href="https://react.dev" target="_blank">
16-
<img src={reactLogo} className="logo react" alt="React logo" />
17-
</a>
18-
</div>
19-
<h1>Vite + React</h1>
20-
<div className="card">
21-
<button onClick={() => setCount((count) => count + 1)}>
22-
count is {count}
23-
</button>
24-
<p>
25-
Edit <code>src/App.tsx</code> and save to test HMR
31+
<div className="mb-6 border-b border-gray-300 pb-3">
32+
<h1 className="text-xl font-medium mb-2 text-black">
33+
Storefront Authentication Demo (PWA)
34+
</h1>
35+
<p className="text-black">
36+
Status:{" "}
37+
{isAuthenticated ? (
38+
<span className="font-semibold text-green-800">
39+
Storefront successfully authenticated
40+
</span>
41+
) : (
42+
<span className="font-semibold text-red-800">
43+
Storefront not authenticated
44+
</span>
45+
)}
2646
</p>
2747
</div>
28-
<p className="read-the-docs">
29-
Click on the Vite and React logos to learn more
30-
</p>
48+
49+
<div>
50+
<h2 className="text-lg font-medium mb-3 text-black">Product List</h2>
51+
{products?.length === 0 ? (
52+
<p className="text-black">No products found.</p>
53+
) : (
54+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
55+
{products?.map((product) => (
56+
<div
57+
key={product.id}
58+
className="bg-white shadow-md rounded-lg p-4 border border-gray-200"
59+
>
60+
<div
61+
className="text-base text-black font-semibold mb-1 truncate"
62+
title={product.attributes?.name}
63+
>
64+
{product.attributes?.name}
65+
</div>
66+
<div className="text-sm text-gray-600">
67+
SKU: {product.attributes?.sku}
68+
</div>
69+
</div>
70+
))}
71+
</div>
72+
)}
73+
</div>
3174
</>
3275
)
3376
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"use client"
2+
3+
import React, { useCallback, useEffect } from "react"
4+
import {
5+
AccessTokenResponse,
6+
client,
7+
createAnAccessToken,
8+
} from "@epcc-sdk/sdks-shopper"
9+
import { CREDENTIALS_COOKIE_KEY } from "../constants"
10+
11+
function tokenExpired(expires: number): boolean {
12+
return Math.floor(Date.now() / 1000) >= expires
13+
}
14+
15+
client.setConfig({
16+
baseUrl: import.meta.env.VITE_APP_EPCC_ENDPOINT_URL!,
17+
})
18+
19+
export function StorefrontProvider({
20+
children,
21+
}: {
22+
children: React.ReactNode
23+
}) {
24+
const interceptor: Parameters<typeof client.interceptors.request.use>[0] =
25+
useCallback(async (request) => {
26+
// Bypass interceptor logic for token requests to prevent infinite loop
27+
if (request.url?.includes("/oauth/access_token")) {
28+
return request
29+
}
30+
31+
let credentials = JSON.parse(
32+
localStorage.getItem(CREDENTIALS_COOKIE_KEY) ?? "{}",
33+
) as AccessTokenResponse | undefined
34+
35+
// check if token expired or missing
36+
if (
37+
!credentials?.access_token ||
38+
(credentials.expires && tokenExpired(credentials.expires))
39+
) {
40+
const clientId = import.meta.env.VITE_APP_EPCC_CLIENT_ID
41+
42+
if (!clientId) {
43+
throw new Error("Missing storefront client id")
44+
}
45+
46+
const authResponse = await createAnAccessToken({
47+
body: {
48+
grant_type: "implicit",
49+
client_id: clientId,
50+
},
51+
})
52+
53+
/**
54+
* Check response did not fail
55+
*/
56+
if (!authResponse.data) {
57+
throw new Error("Failed to get access token")
58+
}
59+
60+
const token = authResponse.data
61+
62+
// Store the credentials in localStorage
63+
localStorage.setItem(CREDENTIALS_COOKIE_KEY, JSON.stringify(token))
64+
credentials = token
65+
}
66+
67+
if (credentials?.access_token) {
68+
request.headers.set(
69+
"Authorization",
70+
`Bearer ${credentials.access_token}`,
71+
)
72+
}
73+
return request
74+
}, [])
75+
76+
// Auto-authenticate if not already authenticated
77+
useEffect(() => {
78+
// Add request interceptor to include the token in requests
79+
client.interceptors.request.use(interceptor)
80+
81+
return () => {
82+
client.interceptors.request.eject(interceptor)
83+
}
84+
}, [])
85+
86+
return <>{children}</>
87+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const EPCC_ENDPOINT_URL = import.meta.env.VITE_APP_EPCC_ENDPOINT_URL
2+
export const COOKIE_PREFIX_KEY = "_store"
3+
export const CREDENTIALS_COOKIE_KEY = `${COOKIE_PREFIX_KEY}_ep_credentials`
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { StrictMode } from 'react'
2-
import { createRoot } from 'react-dom/client'
3-
import './index.css'
4-
import App from './App.tsx'
1+
import React from "react"
2+
import ReactDOM from "react-dom/client"
3+
import App from "./App.tsx"
4+
import "./index.css"
5+
import { StorefrontProvider } from "./auth/StorefrontProvider.tsx"
56

6-
createRoot(document.getElementById('root')!).render(
7-
<StrictMode>
8-
<App />
9-
</StrictMode>,
7+
ReactDOM.createRoot(document.getElementById("root")!).render(
8+
<React.StrictMode>
9+
<StorefrontProvider>
10+
<App />
11+
</StorefrontProvider>
12+
</React.StrictMode>,
1013
)

examples/pwa-authentication/tsconfig.app.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
/* Bundler mode */
1111
"moduleResolution": "bundler",
1212
"allowImportingTsExtensions": true,
13-
"verbatimModuleSyntax": true,
1413
"moduleDetection": "force",
1514
"noEmit": true,
1615
"jsx": "react-jsx",

examples/pwa-authentication/tsconfig.node.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
/* Bundler mode */
1010
"moduleResolution": "bundler",
1111
"allowImportingTsExtensions": true,
12-
"verbatimModuleSyntax": true,
1312
"moduleDetection": "force",
1413
"noEmit": true,
1514

0 commit comments

Comments
 (0)