feat(js-sdk): implement custom storage config to support react native (#11467)
* feat(js-sdk): implement custom storage config to support react native
* chore: add changeset
* feat(js-sdk): implement custom storage config to support react native
* chore: add changeset
* test: ✅ add unit tests for custom storage
This commit is contained in:
5
.changeset/late-comics-turn.md
Normal file
5
.changeset/late-comics-turn.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/js-sdk": minor
|
||||
---
|
||||
|
||||
Implement custom storage configuration option in js-sdk to support react native
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist && tsc -p tsconfig.json && tsc -p tsconfig.esm.json",
|
||||
"test": "jest --passWithNoTests --runInBand --bail --forceExit",
|
||||
"test": "jest --passWithNoTests --runInBand --bail --forceExit --detectOpenHandles",
|
||||
"watch": "tsc --build --watch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { setupServer } from "msw/node"
|
||||
import { Client, FetchError, PUBLISHABLE_KEY_HEADER } from "../client"
|
||||
|
||||
const baseUrl = "https://someurl.com"
|
||||
const token = "token-123"
|
||||
const jwtTokenStorageKey = "medusa_auth_token"
|
||||
|
||||
// This is just a network-layer mocking, it doesn't start an actual server
|
||||
const server = setupServer(
|
||||
@@ -77,7 +79,7 @@ const server = setupServer(
|
||||
return HttpResponse.json({ test: "test" })
|
||||
}),
|
||||
http.get(`${baseUrl}/jwt`, ({ request }) => {
|
||||
if (request.headers.get("authorization") === "Bearer token-123") {
|
||||
if (request.headers.get("authorization") === `Bearer ${token}`) {
|
||||
return HttpResponse.json({
|
||||
test: "test",
|
||||
})
|
||||
@@ -259,9 +261,8 @@ describe("Client", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Authrized requests", () => {
|
||||
describe("Authorized requests", () => {
|
||||
it("should not store the token by default", async () => {
|
||||
const token = "token-123" // Eg. from a response after a successful authentication
|
||||
client.setToken(token)
|
||||
|
||||
const resp = await client.fetch<any>("nostore")
|
||||
@@ -274,13 +275,12 @@ describe("Client", () => {
|
||||
localStorage: { setItem: jest.fn(), getItem: () => token } as any,
|
||||
} as any
|
||||
|
||||
const token = "token-123" // Eg. from a response after a successful authentication
|
||||
client.setToken(token)
|
||||
|
||||
const resp = await client.fetch<any>("jwt")
|
||||
expect(resp).toEqual({ test: "test" })
|
||||
expect(global.window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"medusa_auth_token",
|
||||
jwtTokenStorageKey,
|
||||
token
|
||||
)
|
||||
|
||||
@@ -289,5 +289,105 @@ describe("Client", () => {
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe("Custom Storage", () => {
|
||||
const mockSyncStorage = {
|
||||
storage: new Map<string, string>(),
|
||||
getItem: jest.fn(
|
||||
(key: string) => mockSyncStorage.storage.get(key) || null
|
||||
),
|
||||
setItem: jest.fn((key: string, value: string) =>
|
||||
mockSyncStorage.storage.set(key, value)
|
||||
),
|
||||
removeItem: jest.fn((key: string) => mockSyncStorage.storage.delete(key)),
|
||||
}
|
||||
|
||||
const mockAsyncStorage = {
|
||||
storage: new Map<string, string>(),
|
||||
getItem: jest.fn(
|
||||
async (key: string) => mockAsyncStorage.storage.get(key) || null
|
||||
),
|
||||
setItem: jest.fn(async (key: string, value: string) =>
|
||||
mockAsyncStorage.storage.set(key, value)
|
||||
),
|
||||
removeItem: jest.fn(async (key: string) =>
|
||||
mockAsyncStorage.storage.delete(key)
|
||||
),
|
||||
}
|
||||
|
||||
describe("Synchronous Custom Storage", () => {
|
||||
let client: Client
|
||||
|
||||
beforeEach(() => {
|
||||
mockSyncStorage.storage.clear()
|
||||
client = new Client({
|
||||
baseUrl,
|
||||
auth: {
|
||||
type: "jwt",
|
||||
jwtTokenStorageMethod: "custom",
|
||||
storage: mockSyncStorage,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should store and retrieve token", async () => {
|
||||
await client.setToken(token)
|
||||
expect(mockSyncStorage.setItem).toHaveBeenCalledWith(
|
||||
jwtTokenStorageKey,
|
||||
token
|
||||
)
|
||||
const resp = await client.fetch<any>("jwt")
|
||||
expect(resp).toEqual({ test: "test" })
|
||||
expect(mockSyncStorage.getItem).toHaveBeenCalledWith(jwtTokenStorageKey)
|
||||
})
|
||||
|
||||
it("should clear token", async () => {
|
||||
await client.setToken(token)
|
||||
await client.clearToken()
|
||||
const resp = await client.fetch<any>("nostore")
|
||||
expect(resp).toEqual({ test: "test" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("Asynchronous Custom Storage", () => {
|
||||
let client: Client
|
||||
|
||||
beforeEach(() => {
|
||||
mockAsyncStorage.storage.clear()
|
||||
jest.clearAllMocks()
|
||||
client = new Client({
|
||||
baseUrl,
|
||||
auth: {
|
||||
type: "jwt",
|
||||
jwtTokenStorageMethod: "custom",
|
||||
storage: mockAsyncStorage,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should store and retrieve token asynchronously", async () => {
|
||||
await client.setToken(token)
|
||||
|
||||
expect(mockAsyncStorage.setItem).toHaveBeenCalledWith(
|
||||
jwtTokenStorageKey,
|
||||
token
|
||||
)
|
||||
|
||||
const resp = await client.fetch<any>("jwt")
|
||||
expect(resp).toEqual({ test: "test" })
|
||||
expect(mockAsyncStorage.getItem).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should clear token asynchronously", async () => {
|
||||
await client.setToken(token)
|
||||
await client.clearToken()
|
||||
|
||||
expect(mockAsyncStorage.removeItem).toHaveBeenCalledWith(
|
||||
jwtTokenStorageKey
|
||||
)
|
||||
|
||||
const resp = await client.fetch<any>("nostore")
|
||||
expect(resp).toEqual({ test: "test" })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -175,15 +175,15 @@ export class Client {
|
||||
return { stream: null, abort: abortFunc }
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.setToken_(token)
|
||||
async setToken(token: string) {
|
||||
await this.setToken_(token)
|
||||
}
|
||||
|
||||
clearToken() {
|
||||
this.clearToken_()
|
||||
async clearToken() {
|
||||
await this.clearToken_()
|
||||
}
|
||||
|
||||
protected clearToken_() {
|
||||
protected async clearToken_() {
|
||||
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
||||
switch (storageMethod) {
|
||||
case "local": {
|
||||
@@ -194,6 +194,10 @@ export class Client {
|
||||
window.sessionStorage.removeItem(storageKey)
|
||||
break
|
||||
}
|
||||
case "custom": {
|
||||
await this.config.auth?.storage?.removeItem(storageKey)
|
||||
break
|
||||
}
|
||||
case "memory": {
|
||||
this.token = ""
|
||||
break
|
||||
@@ -219,7 +223,7 @@ export class Client {
|
||||
const headers = new Headers(defaultHeaders)
|
||||
const customHeaders = {
|
||||
...this.config.globalHeaders,
|
||||
...this.getJwtHeader_(),
|
||||
...(await this.getJwtHeader_()),
|
||||
...init?.headers,
|
||||
}
|
||||
// We use `headers.set` in order to ensure headers are overwritten in a case-insensitive manner.
|
||||
@@ -278,17 +282,17 @@ export class Client {
|
||||
: {}
|
||||
}
|
||||
|
||||
protected getJwtHeader_ = (): { Authorization: string } | {} => {
|
||||
protected async getJwtHeader_(): Promise<{ Authorization: string } | {}> {
|
||||
// If the user has requested for session storage, we don't want to send the JWT token in the header.
|
||||
if (this.config.auth?.type === "session") {
|
||||
return {}
|
||||
}
|
||||
|
||||
const token = this.getToken_()
|
||||
const token = await this.getToken_()
|
||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||
}
|
||||
|
||||
protected setToken_ = (token: string) => {
|
||||
protected async setToken_(token: string) {
|
||||
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
||||
switch (storageMethod) {
|
||||
case "local": {
|
||||
@@ -299,6 +303,10 @@ export class Client {
|
||||
window.sessionStorage.setItem(storageKey, token)
|
||||
break
|
||||
}
|
||||
case "custom": {
|
||||
await this.config.auth?.storage?.setItem(storageKey, token)
|
||||
break
|
||||
}
|
||||
case "memory": {
|
||||
this.token = token
|
||||
break
|
||||
@@ -306,7 +314,7 @@ export class Client {
|
||||
}
|
||||
}
|
||||
|
||||
protected getToken_ = () => {
|
||||
protected async getToken_() {
|
||||
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
||||
switch (storageMethod) {
|
||||
case "local": {
|
||||
@@ -315,17 +323,21 @@ export class Client {
|
||||
case "session": {
|
||||
return window.sessionStorage.getItem(storageKey)
|
||||
}
|
||||
case "custom": {
|
||||
return await this.config.auth?.storage?.getItem(storageKey)
|
||||
}
|
||||
case "memory": {
|
||||
return this.token
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
protected getTokenStorageInfo_ = () => {
|
||||
const hasLocal = hasStorage("localStorage")
|
||||
const hasSession = hasStorage("sessionStorage")
|
||||
const hasCustom = Boolean(this.config.auth?.storage)
|
||||
|
||||
const storageMethod =
|
||||
this.config.auth?.jwtTokenStorageMethod ||
|
||||
@@ -334,10 +346,13 @@ export class Client {
|
||||
this.config.auth?.jwtTokenStorageKey || this.DEFAULT_JWT_STORAGE_KEY
|
||||
|
||||
if (!hasLocal && storageMethod === "local") {
|
||||
throw new Error("Local JWT storage is only available in the browser")
|
||||
this.throwError_("Local JWT storage is only available in the browser")
|
||||
}
|
||||
if (!hasSession && storageMethod === "session") {
|
||||
throw new Error("Session JWT storage is only available in the browser")
|
||||
this.throwError_("Session JWT storage is only available in the browser")
|
||||
}
|
||||
if (!hasCustom && storageMethod === "custom") {
|
||||
this.throwError_("Custom storage was not provided in the config")
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -345,4 +360,9 @@ export class Client {
|
||||
storageKey,
|
||||
}
|
||||
}
|
||||
|
||||
protected throwError_(message: string) {
|
||||
this.logger.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,22 @@ export type Config = {
|
||||
auth?: {
|
||||
type?: "jwt" | "session"
|
||||
jwtTokenStorageKey?: string
|
||||
jwtTokenStorageMethod?: "local" | "session" | "memory" | "nostore"
|
||||
jwtTokenStorageMethod?: "local" | "session" | "memory" | "custom" | "nostore"
|
||||
fetchCredentials?: "include" | "omit" | "same-origin"
|
||||
storage?: CustomStorage
|
||||
}
|
||||
logger?: Logger
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
export type Awaitable<T> = T | Promise<T>
|
||||
|
||||
export interface CustomStorage {
|
||||
getItem(key: string): Awaitable<string | null>
|
||||
setItem(key: string, value: string): Awaitable<void>
|
||||
removeItem(key: string): Awaitable<void>
|
||||
}
|
||||
|
||||
export type FetchParams = Parameters<typeof fetch>
|
||||
|
||||
export type ClientHeaders = Record<
|
||||
|
||||
Reference in New Issue
Block a user