diff --git a/.changeset/weak-windows-burn.md b/.changeset/weak-windows-burn.md new file mode 100644 index 0000000000..5ed9d6e8ef --- /dev/null +++ b/.changeset/weak-windows-burn.md @@ -0,0 +1,6 @@ +--- +'demo-store': patch +'@shopify/create-hydrogen': patch +--- + +Improved types of `HydrogenSession` when accessing `session.get('customerAccessToken')`. diff --git a/examples/customer-api/tsconfig.json b/examples/customer-api/tsconfig.json index 90e7fb0044..68a0375c5d 100644 --- a/examples/customer-api/tsconfig.json +++ b/examples/customer-api/tsconfig.json @@ -5,8 +5,9 @@ "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", + "moduleResolution": "Bundler", "resolveJsonModule": true, + "module": "ES2022", "target": "ES2022", "strict": true, "allowJs": true, diff --git a/package-lock.json b/package-lock.json index c563116d84..a6eb7dfc6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29709,8 +29709,7 @@ "react-use": "^17.4.0", "schema-dts": "^1.1.0", "tiny-invariant": "^1.2.0", - "typographic-base": "^1.0.4", - "worktop": "^0.7.3" + "typographic-base": "^1.0.4" }, "devDependencies": { "@remix-run/dev": "1.19.1", @@ -37900,8 +37899,7 @@ "tailwindcss": "^3.3.0", "tiny-invariant": "^1.2.0", "typescript": "^5.2.2", - "typographic-base": "^1.0.4", - "worktop": "^0.7.3" + "typographic-base": "^1.0.4" } }, "depd": { diff --git a/templates/demo-store/app/lib/session.server.ts b/templates/demo-store/app/lib/session.server.ts index 4af86a0a51..7f0550c630 100644 --- a/templates/demo-store/app/lib/session.server.ts +++ b/templates/demo-store/app/lib/session.server.ts @@ -10,12 +10,12 @@ import { * swap out the cookie-based implementation with something else! */ export class HydrogenSession { - constructor( - private sessionStorage: SessionStorage, - private session: Session, - ) { - this.session = session; - this.sessionStorage = sessionStorage; + #sessionStorage; + #session; + + constructor(sessionStorage: SessionStorage, session: Session) { + this.#sessionStorage = sessionStorage; + this.#session = session; } static async init(request: Request, secrets: string[]) { @@ -34,31 +34,31 @@ export class HydrogenSession { return new this(storage, session); } - has(key: string) { - return this.session.has(key); + get has() { + return this.#session.has; } - get(key: string) { - return this.session.get(key); + get get() { + return this.#session.get; } - destroy() { - return this.sessionStorage.destroySession(this.session); + get flash() { + return this.#session.flash; } - flash(key: string, value: any) { - this.session.flash(key, value); + get unset() { + return this.#session.unset; } - unset(key: string) { - this.session.unset(key); + get set() { + return this.#session.set; } - set(key: string, value: any) { - this.session.set(key, value); + destroy() { + return this.#sessionStorage.destroySession(this.#session); } commit() { - return this.sessionStorage.commitSession(this.session); + return this.#sessionStorage.commitSession(this.#session); } } diff --git a/templates/demo-store/app/lib/utils.ts b/templates/demo-store/app/lib/utils.ts index 98e6711442..41752188f0 100644 --- a/templates/demo-store/app/lib/utils.ts +++ b/templates/demo-store/app/lib/utils.ts @@ -1,5 +1,4 @@ import {useLocation, useMatches} from '@remix-run/react'; -import {parse as parseCookie} from 'worktop/cookie'; import type {MoneyV2} from '@shopify/hydrogen/storefront-api-types'; import typographicBase from 'typographic-base'; @@ -332,13 +331,3 @@ export function isLocalPath(url: string) { return false; } - -/** - * Shopify's 'Online Store' stores cart IDs in a 'cart' cookie. - * By doing the same, merchants can switch from the Online Store to Hydrogen - * without customers losing carts. - */ -export function getCartId(request: Request) { - const cookies = parseCookie(request.headers.get('Cookie') || ''); - return cookies.cart ? `gid://shopify/Cart/${cookies.cart}` : undefined; -} diff --git a/templates/demo-store/app/routes/($locale).account.tsx b/templates/demo-store/app/routes/($locale).account.tsx index cc01d3c6de..9afff57f00 100644 --- a/templates/demo-store/app/routes/($locale).account.tsx +++ b/templates/demo-store/app/routes/($locale).account.tsx @@ -51,7 +51,7 @@ export async function loader({request, context, params}: LoaderArgs) { const {pathname} = new URL(request.url); const locale = params.locale; const customerAccessToken = await context.session.get('customerAccessToken'); - const isAuthenticated = Boolean(customerAccessToken); + const isAuthenticated = !!customerAccessToken; const loginPath = locale ? `/${locale}/account/login` : '/account/login'; const isAccountPage = /^\/account\/?$/.test(pathname); diff --git a/templates/demo-store/package.json b/templates/demo-store/package.json index 641c0827e8..d55c888e30 100644 --- a/templates/demo-store/package.json +++ b/templates/demo-store/package.json @@ -34,8 +34,7 @@ "react-use": "^17.4.0", "schema-dts": "^1.1.0", "tiny-invariant": "^1.2.0", - "typographic-base": "^1.0.4", - "worktop": "^0.7.3" + "typographic-base": "^1.0.4" }, "devDependencies": { "@remix-run/dev": "1.19.1", diff --git a/templates/demo-store/remix.env.d.ts b/templates/demo-store/remix.env.d.ts index 6a0a9fd35e..9e0c699134 100644 --- a/templates/demo-store/remix.env.d.ts +++ b/templates/demo-store/remix.env.d.ts @@ -24,10 +24,10 @@ declare global { } } -/** - * Declare local additions to `AppLoadContext` to include the session utilities we injected in `server.ts`. - */ declare module '@shopify/remix-oxygen' { + /** + * Declare local additions to the Remix loader context. + */ export interface AppLoadContext { waitUntil: ExecutionContext['waitUntil']; session: HydrogenSession; @@ -35,6 +35,13 @@ declare module '@shopify/remix-oxygen' { cart: HydrogenCart; env: Env; } + + /** + * Declare the data we expect to access via `context.session`. + */ + export interface SessionData { + customerAccessToken: string; + } } // Needed to make this file a module. diff --git a/templates/demo-store/server.ts b/templates/demo-store/server.ts index 7b03311faf..359d5482ff 100644 --- a/templates/demo-store/server.ts +++ b/templates/demo-store/server.ts @@ -32,7 +32,7 @@ export default { throw new Error('SESSION_SECRET environment variable is not set'); } - const waitUntil = (p: Promise) => executionContext.waitUntil(p); + const waitUntil = executionContext.waitUntil.bind(executionContext); const [cache, session] = await Promise.all([ caches.open('hydrogen'), HydrogenSession.init(request, [env.SESSION_SECRET]), diff --git a/templates/demo-store/tsconfig.json b/templates/demo-store/tsconfig.json index 2f8e9589a6..dcd7c7237a 100644 --- a/templates/demo-store/tsconfig.json +++ b/templates/demo-store/tsconfig.json @@ -5,7 +5,7 @@ "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", + "moduleResolution": "Bundler", "resolveJsonModule": true, "module": "ES2022", "target": "ES2022", diff --git a/templates/hello-world/tsconfig.json b/templates/hello-world/tsconfig.json index 2f8e9589a6..dcd7c7237a 100644 --- a/templates/hello-world/tsconfig.json +++ b/templates/hello-world/tsconfig.json @@ -5,7 +5,7 @@ "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", + "moduleResolution": "Bundler", "resolveJsonModule": true, "module": "ES2022", "target": "ES2022", diff --git a/templates/skeleton/app/root.tsx b/templates/skeleton/app/root.tsx index e76daf9ee2..7fb5592c25 100644 --- a/templates/skeleton/app/root.tsx +++ b/templates/skeleton/app/root.tsx @@ -63,8 +63,8 @@ export async function loader({context}: LoaderArgs) { // validate the customer access token is valid const {isLoggedIn, headers} = await validateCustomerAccessToken( - customerAccessToken, session, + customerAccessToken, ); // defer the cart query by not awaiting it @@ -198,17 +198,19 @@ export function CatchBoundary() { * ``` * */ async function validateCustomerAccessToken( - customerAccessToken: CustomerAccessToken, session: HydrogenSession, + customerAccessToken?: CustomerAccessToken, ) { let isLoggedIn = false; const headers = new Headers(); if (!customerAccessToken?.accessToken || !customerAccessToken?.expiresAt) { return {isLoggedIn, headers}; } - const expiresAt = new Date(customerAccessToken.expiresAt); - const dateNow = new Date(); + + const expiresAt = new Date(customerAccessToken.expiresAt).getTime(); + const dateNow = Date.now(); const customerAccessTokenExpired = expiresAt < dateNow; + if (customerAccessTokenExpired) { session.unset('customerAccessToken'); headers.append('Set-Cookie', await session.commit()); diff --git a/templates/skeleton/app/routes/account.tsx b/templates/skeleton/app/routes/account.tsx index 29032252c7..0bf8c3fe38 100644 --- a/templates/skeleton/app/routes/account.tsx +++ b/templates/skeleton/app/routes/account.tsx @@ -10,7 +10,7 @@ export async function loader({request, context}: LoaderArgs) { const {session, storefront} = context; const {pathname} = new URL(request.url); const customerAccessToken = await session.get('customerAccessToken'); - const isLoggedIn = Boolean(customerAccessToken?.accessToken); + const isLoggedIn = !!customerAccessToken?.accessToken; const isAccountHome = pathname === '/account' || pathname === '/account/'; const isPrivateRoute = /^\/account\/(orders|orders\/.*|profile|addresses|addresses\/.*)$/.test( diff --git a/templates/skeleton/app/routes/cart.tsx b/templates/skeleton/app/routes/cart.tsx index f49778fb7e..8c4510aa49 100644 --- a/templates/skeleton/app/routes/cart.tsx +++ b/templates/skeleton/app/routes/cart.tsx @@ -54,7 +54,7 @@ export async function action({request, context}: ActionArgs) { case CartForm.ACTIONS.BuyerIdentityUpdate: { result = await cart.updateBuyerIdentity({ ...inputs.buyerIdentity, - customerAccessToken, + customerAccessToken: customerAccessToken?.accessToken, }); break; } diff --git a/templates/skeleton/remix.env.d.ts b/templates/skeleton/remix.env.d.ts index b49e8a0f63..e2066539e7 100644 --- a/templates/skeleton/remix.env.d.ts +++ b/templates/skeleton/remix.env.d.ts @@ -6,6 +6,7 @@ import '@total-typescript/ts-reset'; import type {Storefront, HydrogenCart} from '@shopify/hydrogen'; +import type {CustomerAccessToken} from '@shopify/hydrogen/storefront-api-types'; import type {HydrogenSession} from './server'; declare global { @@ -26,10 +27,10 @@ declare global { } } -/** - * Declare local additions to `AppLoadContext` to include the session utilities we injected in `server.ts`. - */ declare module '@shopify/remix-oxygen' { + /** + * Declare local additions to the Remix loader context. + */ export interface AppLoadContext { env: Env; cart: HydrogenCart; @@ -37,4 +38,11 @@ declare module '@shopify/remix-oxygen' { session: HydrogenSession; waitUntil: ExecutionContext['waitUntil']; } + + /** + * Declare the data we expect to access via `context.session`. + */ + export interface SessionData { + customerAccessToken: CustomerAccessToken; + } } diff --git a/templates/skeleton/server.ts b/templates/skeleton/server.ts index 72ec1ce688..7561489cc3 100644 --- a/templates/skeleton/server.ts +++ b/templates/skeleton/server.ts @@ -32,7 +32,7 @@ export default { throw new Error('SESSION_SECRET environment variable is not set'); } - const waitUntil = (p: Promise) => executionContext.waitUntil(p); + const waitUntil = executionContext.waitUntil.bind(executionContext); const [cache, session] = await Promise.all([ caches.open('hydrogen'), HydrogenSession.init(request, [env.SESSION_SECRET]), @@ -99,10 +99,13 @@ export default { * swap out the cookie-based implementation with something else! */ export class HydrogenSession { - constructor( - private sessionStorage: SessionStorage, - private session: Session, - ) {} + #sessionStorage; + #session; + + constructor(sessionStorage: SessionStorage, session: Session) { + this.#sessionStorage = sessionStorage; + this.#session = session; + } static async init(request: Request, secrets: string[]) { const storage = createCookieSessionStorage({ @@ -120,32 +123,32 @@ export class HydrogenSession { return new this(storage, session); } - has(key: string) { - return this.session.has(key); + get has() { + return this.#session.has; } - get(key: string) { - return this.session.get(key); + get get() { + return this.#session.get; } - destroy() { - return this.sessionStorage.destroySession(this.session); + get flash() { + return this.#session.flash; } - flash(key: string, value: any) { - this.session.flash(key, value); + get unset() { + return this.#session.unset; } - unset(key: string) { - this.session.unset(key); + get set() { + return this.#session.set; } - set(key: string, value: any) { - this.session.set(key, value); + destroy() { + return this.#sessionStorage.destroySession(this.#session); } commit() { - return this.sessionStorage.commitSession(this.session); + return this.#sessionStorage.commitSession(this.#session); } } diff --git a/templates/skeleton/tsconfig.json b/templates/skeleton/tsconfig.json index 2f8e9589a6..dcd7c7237a 100644 --- a/templates/skeleton/tsconfig.json +++ b/templates/skeleton/tsconfig.json @@ -5,7 +5,7 @@ "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", + "moduleResolution": "Bundler", "resolveJsonModule": true, "module": "ES2022", "target": "ES2022",