Skip to content

Commit 8c352b5

Browse files
authored
feat(next): support nextjs 15 async cookies (#884)
1 parent 61e8a6a commit 8c352b5

File tree

12 files changed

+1118
-817
lines changed

12 files changed

+1118
-817
lines changed

.changeset/spicy-lizards-kick.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@logto/next": minor
3+
"@logto/node": minor
4+
---
5+
6+
Support Next.js 15 async cookies

packages/next-sample/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
},
1111
"dependencies": {
1212
"@logto/next": "workspace:^",
13-
"next": "^14.2.10",
14-
"react": "^18.2.0",
15-
"react-dom": "^18.2.0",
13+
"next": "^15.0.4",
14+
"react": "^19.0.0",
15+
"react-dom": "^19.0.0",
1616
"swr": "^2.0.0"
1717
},
1818
"devDependencies": {

packages/next-server-actions-sample/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
},
1111
"dependencies": {
1212
"@logto/next": "workspace:^",
13-
"next": "^14.2.10",
14-
"react": "^18.2.0",
15-
"react-dom": "^18.2.0"
13+
"next": "^15.0.4",
14+
"react": "^19.0.0",
15+
"react-dom": "^19.0.0"
1616
},
1717
"devDependencies": {
1818
"@silverhand/ts-config": "^6.0.0",
@@ -21,7 +21,7 @@
2121
"@types/react": "^18.2.56",
2222
"@types/react-dom": "^18.2.19",
2323
"eslint": "^8.50.0",
24-
"eslint-config-next": "^14.0.4",
24+
"eslint-config-next": "^15.0.4",
2525
"lint-staged": "^15.0.0",
2626
"postcss": "^8.4.31",
2727
"postcss-modules": "^6.0.0",

packages/next/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@
6363
"@vitest/coverage-v8": "^1.6.0",
6464
"eslint": "^8.57.0",
6565
"lint-staged": "^15.0.0",
66-
"next": "^14.2.10",
67-
"next-test-api-route-handler": "^4.0.0",
66+
"next": "^15.0.4",
67+
"next-test-api-route-handler": "^4.0.14",
6868
"prettier": "^3.0.0",
69-
"react": "^18.2.0",
70-
"react-dom": "^18.2.0",
69+
"react": "19.0.0",
70+
"react-dom": "19.0.0",
7171
"typescript": "^5.3.3",
7272
"vitest": "^1.6.0"
7373
},

packages/next/server-actions/client.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,15 @@ export default class LogtoClient extends BaseClient {
108108
encryptionKey: this.config.cookieSecret,
109109
cookieKey: `logto_${this.config.appId}`,
110110
isSecure: this.config.cookieSecure,
111-
getCookie: (...args) => {
112-
return cookies().get(...args)?.value ?? '';
111+
getCookie: async (...args) => {
112+
const cookieStore = await cookies();
113+
return cookieStore.get(...args)?.value ?? '';
113114
},
114-
setCookie: (...args) => {
115+
setCookie: async (...args) => {
115116
// In server component (RSC), it is not allowed to modify cookies, see https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options.
116117
if (!ignoreCookieChange) {
117-
cookies().set(...args);
118+
const cookieStore = await cookies();
119+
cookieStore.set(...args);
118120
}
119121
},
120122
});

packages/node/src/utils/cookie-storage.test.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ const createCookieConfig = (
2626
const cookies = new Map(Object.entries(initialCookies));
2727
return {
2828
encryptionKey,
29-
getCookie: (name: string) => cookies.get(name),
30-
setCookie: (name: string, value: string) => cookies.set(name, value),
29+
getCookie: async (name: string) => cookies.get(name),
30+
setCookie: async (name: string, value: string) => {
31+
cookies.set(name, value);
32+
},
3133
...otherConfigs,
3234
};
3335
};
@@ -68,7 +70,8 @@ describe('CookieStorage', () => {
6870

6971
await storage.setItem(PersistKey.AccessToken, 'baz');
7072
expect(storage.data).toEqual({ [PersistKey.AccessToken]: 'baz' });
71-
expect(await unwrapSession(storage.config.getCookie('logtoCookies')!, encryptionKey)).toEqual({
73+
const cookie = await storage.config.getCookie('logtoCookies');
74+
expect(await unwrapSession(cookie ?? '', encryptionKey)).toEqual({
7275
[PersistKey.AccessToken]: 'baz',
7376
});
7477
});
@@ -85,7 +88,8 @@ describe('CookieStorage', () => {
8588

8689
await storage.removeItem(PersistKey.AccessToken);
8790
expect(storage.data).toEqual({});
88-
expect(await unwrapSession(storage.config.getCookie('foo')!, encryptionKey)).toEqual({});
91+
const cookie = await storage.config.getCookie('foo');
92+
expect(await unwrapSession(cookie ?? '', encryptionKey)).toEqual({});
8993
});
9094
});
9195

@@ -137,7 +141,8 @@ describe('CookieStorage concurrency', () => {
137141
};
138142
expect(storage.data).toEqual(result);
139143

140-
const unwrapped = await unwrapSession(storage.config.getCookie('logtoCookies')!, encryptionKey);
144+
const cookie = await storage.config.getCookie('logtoCookies');
145+
const unwrapped = await unwrapSession(cookie ?? '', encryptionKey);
141146

142147
if (StorageClass === NoQueueTestCookieStorage) {
143148
expect(unwrapped).not.toEqual(result);

packages/node/src/utils/cookie-storage.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ export type CookieConfig = {
1414
cookieKey?: string;
1515
/** Set to true in https */
1616
isSecure?: boolean;
17-
getCookie: (name: string) => string | undefined;
17+
getCookie: (name: string) => Promise<string | undefined> | string | undefined;
1818
setCookie: (
1919
name: string,
2020
value: string,
2121
options: CookieSerializeOptions & { path: string }
22-
) => void;
22+
) => Promise<void> | void;
2323
};
2424

2525
/**
@@ -56,7 +56,7 @@ export class CookieStorage implements Storage<PersistKey> {
5656
async init() {
5757
const { encryptionKey } = this.config;
5858
this.sessionData = await unwrapSession(
59-
this.config.getCookie(this.cookieKey) ?? '',
59+
(await this.config.getCookie(this.cookieKey)) ?? '',
6060
encryptionKey
6161
);
6262
}
@@ -88,6 +88,6 @@ export class CookieStorage implements Storage<PersistKey> {
8888
protected async write(data = this.sessionData) {
8989
const { encryptionKey } = this.config;
9090
const value = await wrapSession(data, encryptionKey);
91-
this.config.setCookie(this.cookieKey, value, this.cookieOptions);
91+
await this.config.setCookie(this.cookieKey, value, this.cookieOptions);
9292
}
9393
}

packages/nuxt/src/runtime/composables/use-logto-client.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type LogtoClient from '@logto/node';
2-
31
import { useNuxtApp } from '#app';
2+
import type LogtoClient from '@logto/node';
43

54
/**
65
* Get the Logto client instance in the current context. Returns `undefined` if the client is not

packages/nuxt/src/runtime/composables/use-logto-user.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { useState, useNuxtApp } from '#app';
12
import type { UserInfoResponse } from '@logto/node';
3+
import { shallowRef } from 'vue';
24

35
import { LogtoStateKey } from '../utils/constants';
46

5-
import { shallowRef as shallowReference, useNuxtApp, useState } from '#imports';
6-
77
/**
88
* Get the Logto user information. If the user is not signed in, this composable will return
99
* `undefined`.
@@ -25,7 +25,7 @@ import { shallowRef as shallowReference, useNuxtApp, useState } from '#imports';
2525
export default function useLogtoUser() {
2626
const nuxtApp = useNuxtApp();
2727
const user = useState<UserInfoResponse | undefined>(LogtoStateKey.User, () =>
28-
shallowReference(nuxtApp.ssrContext?.event.context.logtoUser)
28+
shallowRef(nuxtApp.ssrContext?.event.context.logtoUser)
2929
);
3030
return user.value;
3131
}

packages/nuxt/src/runtime/server/event-handler.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import LogtoClient, { CookieStorage } from '@logto/node';
22
import { trySafe } from '@silverhand/essentials';
33
import { defineEventHandler, getRequestURL, getCookie, setCookie, sendRedirect } from 'h3';
44

5+
import { useRuntimeConfig } from '#imports';
6+
57
import { defaults } from '../utils/constants';
68
import { type LogtoRuntimeConfig } from '../utils/types';
79

8-
import { useRuntimeConfig } from '#imports';
9-
1010
export default defineEventHandler(async (event) => {
1111
const config = useRuntimeConfig(event);
1212

@@ -57,8 +57,8 @@ export default defineEventHandler(async (event) => {
5757
cookieKey: cookieName,
5858
encryptionKey: cookieEncryptionKey,
5959
isSecure: cookieSecure,
60-
getCookie: (name) => getCookie(event, name),
61-
setCookie: (name, value, options) => {
60+
getCookie: async (name) => getCookie(event, name),
61+
setCookie: async (name, value, options) => {
6262
setCookie(event, name, value, options);
6363
},
6464
});

packages/react/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"postcss": "^8.4.31",
4747
"prettier": "^3.0.0",
4848
"react": "^18.0.2",
49+
"react-dom": "^18.0.2",
4950
"stylelint": "^16.0.0",
5051
"typescript": "^5.3.3",
5152
"vitest": "^1.6.0"

0 commit comments

Comments
 (0)