@shopify/shop-minis-react 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/index10.js +2 -2
- package/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index5.js +2 -2
- package/dist/_virtual/index6.js +3 -2
- package/dist/_virtual/index6.js.map +1 -1
- package/dist/_virtual/index7.js +2 -3
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/_virtual/index9.js +2 -2
- package/dist/components/atoms/image.js +5 -5
- package/dist/components/atoms/image.js.map +1 -1
- package/dist/components/commerce/product-card.js +20 -20
- package/dist/components/commerce/product-card.js.map +1 -1
- package/dist/hooks/events/useOnAppStateChange.js +14 -0
- package/dist/hooks/events/useOnAppStateChange.js.map +1 -0
- package/dist/hooks/events/useOnMiniBlur.js +14 -0
- package/dist/hooks/events/useOnMiniBlur.js.map +1 -0
- package/dist/hooks/events/useOnMiniClose.js +14 -0
- package/dist/hooks/events/useOnMiniClose.js.map +1 -0
- package/dist/hooks/events/useOnMiniFocus.js +14 -0
- package/dist/hooks/events/useOnMiniFocus.js.map +1 -0
- package/dist/hooks/user/useGenerateUserToken.js +28 -6
- package/dist/hooks/user/useGenerateUserToken.js.map +1 -1
- package/dist/index.js +114 -106
- package/dist/index.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/index.js +1 -1
- package/package.json +2 -2
- package/src/components/atoms/image.tsx +1 -1
- package/src/components/commerce/product-card.tsx +3 -3
- package/src/hooks/events/useOnAppStateChange.ts +20 -0
- package/src/hooks/events/useOnMiniBlur.ts +16 -0
- package/src/hooks/events/useOnMiniClose.ts +16 -0
- package/src/hooks/events/useOnMiniFocus.ts +16 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/user/useGenerateUserToken.test.ts +340 -0
- package/src/hooks/user/useGenerateUserToken.ts +72 -2
- package/src/stories/ImageContentWrapper.stories.tsx +1 -4
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import {renderHook, act} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi, beforeEach} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useHandleAction} from '../../internal/useHandleAction'
|
|
5
|
+
import {useShopActions} from '../../internal/useShopActions'
|
|
6
|
+
|
|
7
|
+
import {useGenerateUserToken} from './useGenerateUserToken'
|
|
8
|
+
|
|
9
|
+
// Mock the internal hooks
|
|
10
|
+
vi.mock('../../internal/useShopActions', () => ({
|
|
11
|
+
useShopActions: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
vi.mock('../../internal/useHandleAction', () => ({
|
|
15
|
+
useHandleAction: vi.fn((action: any) => action),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
describe('useGenerateUserToken', () => {
|
|
19
|
+
const mockToken = 'test-token-123'
|
|
20
|
+
const mockUserState = 'VERIFIED'
|
|
21
|
+
|
|
22
|
+
// Helper to create a future timestamp
|
|
23
|
+
const getFutureTimestamp = (hoursFromNow: number) => {
|
|
24
|
+
const date = new Date()
|
|
25
|
+
date.setHours(date.getHours() + hoursFromNow)
|
|
26
|
+
return date.toISOString()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Helper to create an expired timestamp
|
|
30
|
+
const getExpiredTimestamp = () => {
|
|
31
|
+
const date = new Date()
|
|
32
|
+
date.setHours(date.getHours() - 1)
|
|
33
|
+
return date.toISOString()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const createMockResponse = (overrides = {}) => ({
|
|
37
|
+
data: {
|
|
38
|
+
token: mockToken,
|
|
39
|
+
expiresAt: getFutureTimestamp(24), // 24 hours from now
|
|
40
|
+
userState: mockUserState,
|
|
41
|
+
...overrides,
|
|
42
|
+
},
|
|
43
|
+
userErrors: [],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
let mockGenerateUserToken: ReturnType<typeof vi.fn>
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks()
|
|
50
|
+
|
|
51
|
+
mockGenerateUserToken = vi.fn()
|
|
52
|
+
;(useShopActions as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
53
|
+
generateUserToken: mockGenerateUserToken,
|
|
54
|
+
})
|
|
55
|
+
;(useHandleAction as ReturnType<typeof vi.fn>).mockImplementation(
|
|
56
|
+
(action: any) => action
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('Token Generation', () => {
|
|
61
|
+
it('should generate a new token on first call', async () => {
|
|
62
|
+
mockGenerateUserToken.mockResolvedValue(createMockResponse())
|
|
63
|
+
|
|
64
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
65
|
+
|
|
66
|
+
let tokenResponse: any
|
|
67
|
+
await act(async () => {
|
|
68
|
+
tokenResponse = await result.current.generateUserToken()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(tokenResponse.data).toEqual({
|
|
72
|
+
token: mockToken,
|
|
73
|
+
expiresAt: expect.any(String),
|
|
74
|
+
userState: mockUserState,
|
|
75
|
+
})
|
|
76
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should throw error when token generation fails', async () => {
|
|
80
|
+
const error = new Error('Network error')
|
|
81
|
+
mockGenerateUserToken.mockRejectedValue(error)
|
|
82
|
+
|
|
83
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
84
|
+
|
|
85
|
+
await expect(result.current.generateUserToken()).rejects.toThrow(
|
|
86
|
+
'Network error'
|
|
87
|
+
)
|
|
88
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should return response even when token is incomplete', async () => {
|
|
92
|
+
const incompleteResponse = {
|
|
93
|
+
data: {
|
|
94
|
+
token: null,
|
|
95
|
+
expiresAt: null,
|
|
96
|
+
userState: null,
|
|
97
|
+
},
|
|
98
|
+
userErrors: [
|
|
99
|
+
{code: 'INVALID_TOKEN', message: 'Failed to generate token'},
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
mockGenerateUserToken.mockResolvedValue(incompleteResponse)
|
|
104
|
+
|
|
105
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
106
|
+
|
|
107
|
+
const response = await result.current.generateUserToken()
|
|
108
|
+
|
|
109
|
+
expect(response).toEqual(incompleteResponse)
|
|
110
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('Token Caching', () => {
|
|
115
|
+
it('should return cached token on subsequent calls', async () => {
|
|
116
|
+
mockGenerateUserToken.mockResolvedValue(createMockResponse())
|
|
117
|
+
|
|
118
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
119
|
+
|
|
120
|
+
// First call - should hit the API
|
|
121
|
+
let firstResponse: any
|
|
122
|
+
await act(async () => {
|
|
123
|
+
firstResponse = await result.current.generateUserToken()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Second call - should return cached token
|
|
127
|
+
let secondResponse: any
|
|
128
|
+
await act(async () => {
|
|
129
|
+
secondResponse = await result.current.generateUserToken()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Both should be the same reference (cached)
|
|
133
|
+
expect(firstResponse).toBe(secondResponse)
|
|
134
|
+
expect(secondResponse.data.token).toBe(mockToken)
|
|
135
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(1) // Only called once
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should request new token when cached token is expired', async () => {
|
|
139
|
+
mockGenerateUserToken
|
|
140
|
+
.mockResolvedValueOnce(
|
|
141
|
+
createMockResponse({
|
|
142
|
+
token: 'expired-token',
|
|
143
|
+
expiresAt: getExpiredTimestamp(),
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
.mockResolvedValueOnce(
|
|
147
|
+
createMockResponse({
|
|
148
|
+
token: 'new-token',
|
|
149
|
+
expiresAt: getFutureTimestamp(24),
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
154
|
+
|
|
155
|
+
// First call - gets expired token
|
|
156
|
+
await act(async () => {
|
|
157
|
+
await result.current.generateUserToken()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Second call - should request new token since first is expired
|
|
161
|
+
let secondResponse: any
|
|
162
|
+
await act(async () => {
|
|
163
|
+
secondResponse = await result.current.generateUserToken()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(secondResponse.data.token).toBe('new-token')
|
|
167
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(2)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('should request new token when cached token is within 5-minute buffer', async () => {
|
|
171
|
+
// Token expires in 4 minutes (within buffer)
|
|
172
|
+
mockGenerateUserToken
|
|
173
|
+
.mockResolvedValueOnce(
|
|
174
|
+
createMockResponse({
|
|
175
|
+
token: 'almost-expired-token',
|
|
176
|
+
expiresAt: getFutureTimestamp(0.066), // ~4 minutes
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
.mockResolvedValueOnce(
|
|
180
|
+
createMockResponse({
|
|
181
|
+
token: 'fresh-token',
|
|
182
|
+
expiresAt: getFutureTimestamp(24),
|
|
183
|
+
})
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
187
|
+
|
|
188
|
+
// First call
|
|
189
|
+
await act(async () => {
|
|
190
|
+
await result.current.generateUserToken()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Second call - should get new token due to buffer
|
|
194
|
+
let secondResponse: any
|
|
195
|
+
await act(async () => {
|
|
196
|
+
secondResponse = await result.current.generateUserToken()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
expect(secondResponse.data.token).toBe('fresh-token')
|
|
200
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(2)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should use cached token when outside 5-minute buffer', async () => {
|
|
204
|
+
// Token expires in 1 hour (well outside the 5-minute buffer)
|
|
205
|
+
const mockResponse = createMockResponse({
|
|
206
|
+
token: 'valid-token',
|
|
207
|
+
expiresAt: getFutureTimestamp(1), // 1 hour from now
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
mockGenerateUserToken.mockResolvedValue(mockResponse)
|
|
211
|
+
|
|
212
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
213
|
+
|
|
214
|
+
// First call
|
|
215
|
+
const firstResponse = await result.current.generateUserToken()
|
|
216
|
+
|
|
217
|
+
// Verify the response has the expected structure
|
|
218
|
+
expect(firstResponse.data.token).toBe('valid-token')
|
|
219
|
+
expect(firstResponse.data.expiresAt).toBeDefined()
|
|
220
|
+
|
|
221
|
+
// Second call - should use cached token
|
|
222
|
+
const secondResponse = await result.current.generateUserToken()
|
|
223
|
+
|
|
224
|
+
// Both responses should be the same cached object
|
|
225
|
+
expect(firstResponse).toBe(secondResponse)
|
|
226
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('Race Condition Prevention', () => {
|
|
231
|
+
it('should handle concurrent requests by returning same promise', async () => {
|
|
232
|
+
// Create a delayed promise to simulate an API call
|
|
233
|
+
const mockResponse = createMockResponse()
|
|
234
|
+
mockGenerateUserToken.mockImplementation(
|
|
235
|
+
() =>
|
|
236
|
+
new Promise(resolve => {
|
|
237
|
+
setTimeout(() => resolve(mockResponse), 10)
|
|
238
|
+
})
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
242
|
+
|
|
243
|
+
// Make two concurrent calls
|
|
244
|
+
const promise1 = result.current.generateUserToken()
|
|
245
|
+
const promise2 = result.current.generateUserToken()
|
|
246
|
+
|
|
247
|
+
// Only one API call should be made
|
|
248
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
|
|
249
|
+
|
|
250
|
+
// Wait for both promises
|
|
251
|
+
const [response1, response2] = await Promise.all([promise1, promise2])
|
|
252
|
+
|
|
253
|
+
// Both should return the same response
|
|
254
|
+
expect(response1).toEqual(mockResponse)
|
|
255
|
+
expect(response2).toEqual(mockResponse)
|
|
256
|
+
expect(response1).toBe(response2)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should handle multiple rapid sequential calls correctly', async () => {
|
|
260
|
+
mockGenerateUserToken.mockResolvedValue(createMockResponse())
|
|
261
|
+
|
|
262
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
263
|
+
|
|
264
|
+
// Make multiple rapid calls
|
|
265
|
+
const promises = Array.from({length: 5}, () =>
|
|
266
|
+
result.current.generateUserToken()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// All should resolve to the same token
|
|
270
|
+
const responses = await Promise.all(promises)
|
|
271
|
+
|
|
272
|
+
responses.forEach(response => {
|
|
273
|
+
expect(response.data.token).toBe(mockToken)
|
|
274
|
+
// All should be the same object reference
|
|
275
|
+
expect(response).toBe(responses[0])
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Should only have made one API call
|
|
279
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(1)
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('Error Handling', () => {
|
|
284
|
+
it('should clear cache on error and retry', async () => {
|
|
285
|
+
mockGenerateUserToken
|
|
286
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
287
|
+
.mockResolvedValueOnce(createMockResponse())
|
|
288
|
+
|
|
289
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
290
|
+
|
|
291
|
+
// First call - should fail
|
|
292
|
+
await expect(result.current.generateUserToken()).rejects.toThrow(
|
|
293
|
+
'Network error'
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
// Second call - should succeed with new request
|
|
297
|
+
let response: any
|
|
298
|
+
await act(async () => {
|
|
299
|
+
response = await result.current.generateUserToken()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
expect(response.data.token).toBe(mockToken)
|
|
303
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(2)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should clear pending request on error', async () => {
|
|
307
|
+
let rejectPromise: any
|
|
308
|
+
const promise = new Promise((_resolve, reject) => {
|
|
309
|
+
rejectPromise = reject
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
mockGenerateUserToken.mockReturnValue(promise)
|
|
313
|
+
|
|
314
|
+
const {result} = renderHook(() => useGenerateUserToken())
|
|
315
|
+
|
|
316
|
+
// Make concurrent calls that will fail
|
|
317
|
+
const promise1 = result.current.generateUserToken()
|
|
318
|
+
const promise2 = result.current.generateUserToken()
|
|
319
|
+
|
|
320
|
+
// Reject the promise
|
|
321
|
+
rejectPromise(new Error('Network error'))
|
|
322
|
+
|
|
323
|
+
// Both should fail with same error
|
|
324
|
+
await expect(promise1).rejects.toThrow('Network error')
|
|
325
|
+
await expect(promise2).rejects.toThrow('Network error')
|
|
326
|
+
|
|
327
|
+
// Reset mock for next call
|
|
328
|
+
mockGenerateUserToken.mockResolvedValue(createMockResponse())
|
|
329
|
+
|
|
330
|
+
// Next call should make a fresh request
|
|
331
|
+
let response: any
|
|
332
|
+
await act(async () => {
|
|
333
|
+
response = await result.current.generateUserToken()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
expect(response.data.token).toBe(mockToken)
|
|
337
|
+
expect(mockGenerateUserToken).toHaveBeenCalledTimes(2) // First failed call + retry
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {useCallback, useRef} from 'react'
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
GeneratedTokenData,
|
|
3
5
|
UserTokenGenerateUserErrors,
|
|
@@ -9,6 +11,8 @@ import {useShopActions} from '../../internal/useShopActions'
|
|
|
9
11
|
interface UseGenerateUserTokenReturns {
|
|
10
12
|
/**
|
|
11
13
|
* Generates a temporary token for the user.
|
|
14
|
+
* Tokens are cached in memory and reused if still valid (with a 5-minute expiry buffer).
|
|
15
|
+
* A new token is automatically generated when the cached token is expired or missing.
|
|
12
16
|
*/
|
|
13
17
|
generateUserToken: () => Promise<{
|
|
14
18
|
data: GeneratedTokenData
|
|
@@ -16,10 +20,76 @@ interface UseGenerateUserTokenReturns {
|
|
|
16
20
|
}>
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
interface CachedTokenResponse {
|
|
24
|
+
data: GeneratedTokenData
|
|
25
|
+
userErrors?: UserTokenGenerateUserErrors[]
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
export function useGenerateUserToken(): UseGenerateUserTokenReturns {
|
|
20
|
-
const {generateUserToken} = useShopActions()
|
|
29
|
+
const {generateUserToken: generateUserTokenAction} = useShopActions()
|
|
30
|
+
const wrappedGenerateToken = useHandleAction(generateUserTokenAction)
|
|
31
|
+
|
|
32
|
+
const cachedResponse = useRef<CachedTokenResponse | null>(null)
|
|
33
|
+
const pendingRequest = useRef<Promise<CachedTokenResponse> | null>(null)
|
|
34
|
+
|
|
35
|
+
const isTokenValid = useCallback(
|
|
36
|
+
(response: CachedTokenResponse | null): boolean => {
|
|
37
|
+
if (!response?.data?.token || !response.data.expiresAt) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const expiryTime = new Date(response.data.expiresAt).getTime()
|
|
43
|
+
const now = Date.now()
|
|
44
|
+
// 5 minutes buffer to ensure token doesn't expire mid-request
|
|
45
|
+
const bufferTime = 5 * 60 * 1000
|
|
46
|
+
|
|
47
|
+
return expiryTime - bufferTime > now
|
|
48
|
+
} catch {
|
|
49
|
+
// If date parsing fails, consider token invalid
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const generateUserToken =
|
|
57
|
+
useCallback(async (): Promise<CachedTokenResponse> => {
|
|
58
|
+
// Check if cached token exists and is still valid
|
|
59
|
+
if (cachedResponse.current && isTokenValid(cachedResponse.current)) {
|
|
60
|
+
return cachedResponse.current
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If there's already a pending request, return the same promise
|
|
64
|
+
if (pendingRequest.current) {
|
|
65
|
+
return pendingRequest.current
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create new request and store the promise
|
|
69
|
+
pendingRequest.current = (async () => {
|
|
70
|
+
try {
|
|
71
|
+
const response = await wrappedGenerateToken()
|
|
72
|
+
|
|
73
|
+
// Only cache if we got a valid token
|
|
74
|
+
if (response.data?.token && response.data?.expiresAt) {
|
|
75
|
+
cachedResponse.current = response
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return response
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// Clear cache on error to ensure fresh token on next attempt
|
|
81
|
+
cachedResponse.current = null
|
|
82
|
+
throw error
|
|
83
|
+
} finally {
|
|
84
|
+
// Clear pending request after completion (success or failure)
|
|
85
|
+
pendingRequest.current = null
|
|
86
|
+
}
|
|
87
|
+
})()
|
|
88
|
+
|
|
89
|
+
return pendingRequest.current
|
|
90
|
+
}, [wrappedGenerateToken, isTokenValid])
|
|
21
91
|
|
|
22
92
|
return {
|
|
23
|
-
generateUserToken
|
|
93
|
+
generateUserToken,
|
|
24
94
|
}
|
|
25
95
|
}
|
|
@@ -6,7 +6,7 @@ import {injectMocks} from '../mocks'
|
|
|
6
6
|
import type {Meta, StoryObj} from '@storybook/react-vite'
|
|
7
7
|
|
|
8
8
|
const meta = {
|
|
9
|
-
title: '
|
|
9
|
+
title: 'Atoms/ImageContentWrapper',
|
|
10
10
|
component: ImageContentWrapper,
|
|
11
11
|
parameters: {
|
|
12
12
|
layout: 'padded',
|
|
@@ -30,9 +30,6 @@ const meta = {
|
|
|
30
30
|
className: {
|
|
31
31
|
control: 'text',
|
|
32
32
|
},
|
|
33
|
-
Loader: {
|
|
34
|
-
control: 'text',
|
|
35
|
-
},
|
|
36
33
|
},
|
|
37
34
|
tags: ['autodocs'],
|
|
38
35
|
} satisfies Meta<typeof ImageContentWrapper>
|