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:
1 parent
ee848bf0f4
commit
32ad13813b
5 files changed
+155
-21
No files matched your search
@@ -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": {
|
"scripts": {
|
||||||
"build": "rimraf dist && tsc -p tsconfig.json && tsc -p tsconfig.esm.json",
|
"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"
|
"watch": "tsc --build --watch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ import { setupServer } from "msw/node"
|
|||||||
import { Client, FetchError, PUBLISHABLE_KEY_HEADER } from "../client"
|
import { Client, FetchError, PUBLISHABLE_KEY_HEADER } from "../client"
|
||||||
|
|
||||||
const baseUrl = "https://someurl.com"
|
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
|
// This is just a network-layer mocking, it doesn't start an actual server
|
||||||
const server = setupServer(
|
const server = setupServer(
|
||||||
@@ -77,7 +79,7 @@ const server = setupServer(
|
|||||||
return HttpResponse.json({ test: "test" })
|
return HttpResponse.json({ test: "test" })
|
||||||
}),
|
}),
|
||||||
http.get(`${baseUrl}/jwt`, ({ request }) => {
|
http.get(`${baseUrl}/jwt`, ({ request }) => {
|
||||||
if (request.headers.get("authorization") === "Bearer token-123") {
|
if (request.headers.get("authorization") === `Bearer ${token}`) {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
test: "test",
|
test: "test",
|
||||||
})
|
})
|
||||||
@@ -259,9 +261,8 @@ describe("Client", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Authrized requests", () => {
|
describe("Authorized requests", () => {
|
||||||
it("should not store the token by default", async () => {
|
it("should not store the token by default", async () => {
|
||||||
const token = "token-123" // Eg. from a response after a successful authentication
|
|
||||||
client.setToken(token)
|
client.setToken(token)
|
||||||
|
|
||||||
const resp = await client.fetch<any>("nostore")
|
const resp = await client.fetch<any>("nostore")
|
||||||
@@ -274,13 +275,12 @@ describe("Client", () => {
|
|||||||
localStorage: { setItem: jest.fn(), getItem: () => token } as any,
|
localStorage: { setItem: jest.fn(), getItem: () => token } as any,
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
const token = "token-123" // Eg. from a response after a successful authentication
|
|
||||||
client.setToken(token)
|
client.setToken(token)
|
||||||
|
|
||||||
const resp = await client.fetch<any>("jwt")
|
const resp = await client.fetch<any>("jwt")
|
||||||
expect(resp).toEqual({ test: "test" })
|
expect(resp).toEqual({ test: "test" })
|
||||||
expect(global.window.localStorage.setItem).toHaveBeenCalledWith(
|
expect(global.window.localStorage.setItem).toHaveBeenCalledWith(
|
||||||
"medusa_auth_token",
|
jwtTokenStorageKey,
|
||||||
token
|
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 }
|
return { stream: null, abort: abortFunc }
|
||||||
}
|
}
|
||||||
|
|
||||||
setToken(token: string) {
|
async setToken(token: string) {
|
||||||
this.setToken_(token)
|
await this.setToken_(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearToken() {
|
async clearToken() {
|
||||||
this.clearToken_()
|
await this.clearToken_()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected clearToken_() {
|
protected async clearToken_() {
|
||||||
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
||||||
switch (storageMethod) {
|
switch (storageMethod) {
|
||||||
case "local": {
|
case "local": {
|
||||||
@@ -194,6 +194,10 @@ export class Client {
|
|||||||
window.sessionStorage.removeItem(storageKey)
|
window.sessionStorage.removeItem(storageKey)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "custom": {
|
||||||
|
await this.config.auth?.storage?.removeItem(storageKey)
|
||||||
|
break
|
||||||
|
}
|
||||||
case "memory": {
|
case "memory": {
|
||||||
this.token = ""
|
this.token = ""
|
||||||
break
|
break
|
||||||
@@ -219,7 +223,7 @@ export class Client {
|
|||||||
const headers = new Headers(defaultHeaders)
|
const headers = new Headers(defaultHeaders)
|
||||||
const customHeaders = {
|
const customHeaders = {
|
||||||
...this.config.globalHeaders,
|
...this.config.globalHeaders,
|
||||||
...this.getJwtHeader_(),
|
...(await this.getJwtHeader_()),
|
||||||
...init?.headers,
|
...init?.headers,
|
||||||
}
|
}
|
||||||
// We use `headers.set` in order to ensure headers are overwritten in a case-insensitive manner.
|
// 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 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") {
|
if (this.config.auth?.type === "session") {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = this.getToken_()
|
const token = await this.getToken_()
|
||||||
return token ? { Authorization: `Bearer ${token}` } : {}
|
return token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setToken_ = (token: string) => {
|
protected async setToken_(token: string) {
|
||||||
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
||||||
switch (storageMethod) {
|
switch (storageMethod) {
|
||||||
case "local": {
|
case "local": {
|
||||||
@@ -299,6 +303,10 @@ export class Client {
|
|||||||
window.sessionStorage.setItem(storageKey, token)
|
window.sessionStorage.setItem(storageKey, token)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "custom": {
|
||||||
|
await this.config.auth?.storage?.setItem(storageKey, token)
|
||||||
|
break
|
||||||
|
}
|
||||||
case "memory": {
|
case "memory": {
|
||||||
this.token = token
|
this.token = token
|
||||||
break
|
break
|
||||||
@@ -306,7 +314,7 @@ export class Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getToken_ = () => {
|
protected async getToken_() {
|
||||||
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
const { storageMethod, storageKey } = this.getTokenStorageInfo_()
|
||||||
switch (storageMethod) {
|
switch (storageMethod) {
|
||||||
case "local": {
|
case "local": {
|
||||||
@@ -315,17 +323,21 @@ export class Client {
|
|||||||
case "session": {
|
case "session": {
|
||||||
return window.sessionStorage.getItem(storageKey)
|
return window.sessionStorage.getItem(storageKey)
|
||||||
}
|
}
|
||||||
|
case "custom": {
|
||||||
|
return await this.config.auth?.storage?.getItem(storageKey)
|
||||||
|
}
|
||||||
case "memory": {
|
case "memory": {
|
||||||
return this.token
|
return this.token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTokenStorageInfo_ = () => {
|
protected getTokenStorageInfo_ = () => {
|
||||||
const hasLocal = hasStorage("localStorage")
|
const hasLocal = hasStorage("localStorage")
|
||||||
const hasSession = hasStorage("sessionStorage")
|
const hasSession = hasStorage("sessionStorage")
|
||||||
|
const hasCustom = Boolean(this.config.auth?.storage)
|
||||||
|
|
||||||
const storageMethod =
|
const storageMethod =
|
||||||
this.config.auth?.jwtTokenStorageMethod ||
|
this.config.auth?.jwtTokenStorageMethod ||
|
||||||
@@ -334,10 +346,13 @@ export class Client {
|
|||||||
this.config.auth?.jwtTokenStorageKey || this.DEFAULT_JWT_STORAGE_KEY
|
this.config.auth?.jwtTokenStorageKey || this.DEFAULT_JWT_STORAGE_KEY
|
||||||
|
|
||||||
if (!hasLocal && storageMethod === "local") {
|
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") {
|
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 {
|
return {
|
||||||
@@ -345,4 +360,9 @@ export class Client {
|
|||||||
storageKey,
|
storageKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected throwError_(message: string) {
|
||||||
|
this.logger.error(message)
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,13 +13,22 @@ export type Config = {
|
|||||||
auth?: {
|
auth?: {
|
||||||
type?: "jwt" | "session"
|
type?: "jwt" | "session"
|
||||||
jwtTokenStorageKey?: string
|
jwtTokenStorageKey?: string
|
||||||
jwtTokenStorageMethod?: "local" | "session" | "memory" | "nostore"
|
jwtTokenStorageMethod?: "local" | "session" | "memory" | "custom" | "nostore"
|
||||||
fetchCredentials?: "include" | "omit" | "same-origin"
|
fetchCredentials?: "include" | "omit" | "same-origin"
|
||||||
|
storage?: CustomStorage
|
||||||
}
|
}
|
||||||
logger?: Logger
|
logger?: Logger
|
||||||
debug?: boolean
|
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 FetchParams = Parameters<typeof fetch>
|
||||||
|
|
||||||
export type ClientHeaders = Record<
|
export type ClientHeaders = Record<
|
||||||
|
|||||||
Reference in new issue
Block a user