@raxonltd/raxon-core 1.1.6 → 1.1.8

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.
Files changed (66) hide show
  1. package/core/component/general.image.tsx +86 -0
  2. package/core/context/cart.context.tsx +446 -0
  3. package/core/context/security.context.tsx +151 -0
  4. package/core/feature/address/api/places.api.ts +76 -0
  5. package/core/feature/address/form/address-search-input.tsx +125 -0
  6. package/core/feature/address/hook/use.addres.tsx +63 -0
  7. package/core/feature/address/hook/use.address-autocomplete.ts +116 -0
  8. package/core/feature/address/util/address.types.ts +38 -0
  9. package/core/feature/address/util/parse-google-place.ts +66 -0
  10. package/core/feature/analytic-event/analytic.event.api.ts +27 -0
  11. package/core/feature/analytic-event/analytic.event.context.tsx +180 -0
  12. package/core/feature/analytic-event/analytic.event.util.ts +42 -0
  13. package/core/feature/analytic-event/use.analytic.auto.tsx +114 -0
  14. package/core/feature/article/hook/use.article.tsx +33 -0
  15. package/core/feature/attribute/hook/use.attribute.tsx +24 -0
  16. package/core/feature/auth/hook/use.auth.tsx +141 -0
  17. package/core/feature/auth/modal/modal.auth.tsx +80 -0
  18. package/core/feature/auth/view/view.login.tsx +199 -0
  19. package/core/feature/auth/view/view.register.tsx +333 -0
  20. package/core/feature/bank-account/hook/use.bank.account.tsx +47 -0
  21. package/core/feature/brand/hook/use.brand.tsx +24 -0
  22. package/core/feature/cart/component/cart.order.summary.tsx +89 -0
  23. package/core/feature/cart/component/cart.promo.code.section.tsx +208 -0
  24. package/core/feature/cart/hook/use.cart.tsx +267 -0
  25. package/core/feature/cart/util/basket-pay.response.ts +67 -0
  26. package/core/feature/cart/util/cart-optimistic.ts +425 -0
  27. package/core/feature/cart/util/garanti-payment.ts +27 -0
  28. package/core/feature/collection/hook/use.collection.tsx +32 -0
  29. package/core/feature/delivery-method/hook/use.delivery.method.tsx +40 -0
  30. package/core/feature/delivery-method/util/checkout.delivery.method.ts +11 -0
  31. package/core/feature/faq/hook/use.faq.tsx +23 -0
  32. package/core/feature/favorite/hook/use.favorite.tsx +48 -0
  33. package/core/feature/form-submit/form/form.contact.tsx +118 -0
  34. package/core/feature/form-submit/hook/use.form.submit.tsx +16 -0
  35. package/core/feature/invoice/hook/use.invoice.tsx +51 -0
  36. package/core/feature/newsletter/hook/use.newsletter.tsx +124 -0
  37. package/core/feature/newsletter/modal/modal.newsletter.product.tsx +163 -0
  38. package/core/feature/order/hook/use.order.tsx +31 -0
  39. package/core/feature/payment-method/checkout.payment.options.ts +117 -0
  40. package/core/feature/payment-method/hook/use.payment.method.tsx +44 -0
  41. package/core/feature/product/hook/use.product.tsx +122 -0
  42. package/core/feature/profile/hook/use.profile.tsx +126 -0
  43. package/core/feature/promo-code/hook/use.promo.code.tsx +27 -0
  44. package/core/interface/basket.interface.ts +360 -0
  45. package/core/interface/bootstrap.interface.ts +39 -0
  46. package/core/interface/context.interface.ts +9 -0
  47. package/core/interface/inventory.interface.ts +88 -0
  48. package/core/interface/nexine.interface.ts +4 -0
  49. package/core/interface/prisma.interface.ts +8844 -0
  50. package/core/interface/product.interface.ts +111 -0
  51. package/core/raxon.context.tsx +256 -0
  52. package/core/schema/checkout.schema.ts +103 -0
  53. package/core/util/basket.item.display.ts +19 -0
  54. package/core/util/category.nav.ts +46 -0
  55. package/core/util/client-ip.ts +35 -0
  56. package/core/util/collection.util.ts +433 -0
  57. package/core/util/fetch.bootstrap.ts +21 -0
  58. package/core/util/garanti-payment.ts +5 -0
  59. package/core/util/nexine.axios.tsx +104 -0
  60. package/core/util/no-cache.ts +6 -0
  61. package/core/util/util.ts +191 -0
  62. package/core/view/view.checkout.tsx +1964 -0
  63. package/dist/core/view/view.checkout.js +2 -2
  64. package/dist/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +12 -3
  66. package/tailwind.css +11 -0
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Image from 'next/image';
5
+
6
+ interface GeneralImageProps {
7
+ src?: string;
8
+ alt: string;
9
+
10
+ width?: number;
11
+ height?: number;
12
+ className?: string;
13
+ fill?: boolean;
14
+ aspectRatio?: string;
15
+ style?: React.CSSProperties;
16
+ wrapperClassName?: string;
17
+ sizes?: string;
18
+ priority?: boolean;
19
+ quality?: number;
20
+ placeholder?: 'blur' | 'empty';
21
+ blurDataURL?: string;
22
+ onLoad?: () => void;
23
+ onError?: () => void;
24
+ loading?: 'lazy' | 'eager';
25
+ fetchPriority?: 'low' | 'high';
26
+ objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
27
+ fallback?: 'placeholder' | 'hide' | 'error';
28
+ }
29
+
30
+ const GeneralImage = (props: GeneralImageProps) => {
31
+ // Placeholder image URL
32
+ const placeholderImage = 'https://placehold.co/600x400';
33
+ const [isLoading, setIsLoading] = useState(true);
34
+
35
+ if(!props.src) {
36
+ return null;
37
+ }
38
+
39
+ let mageData = (
40
+ <>
41
+ {isLoading && (
42
+ <div className="absolute inset-0 flex items-center justify-center bg-gray-100">
43
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
44
+ </div>
45
+ )}
46
+ <Image
47
+ placeholder="empty"
48
+ src={props.src!}
49
+ alt={props.alt}
50
+ fill={props.fill}
51
+ style={{
52
+ ...props.style,
53
+ opacity: isLoading ? 0 : 1,
54
+ transition: 'opacity 0.3s ease-in-out'
55
+ }}
56
+ className={props.className}
57
+ width={props.width}
58
+ height={props.height}
59
+ priority={props.priority}
60
+ quality={props.quality ?? 80}
61
+ objectFit={props.objectFit}
62
+ fetchPriority={props.fetchPriority}
63
+ onLoad={() => {
64
+ setIsLoading(false);
65
+ props.onLoad?.();
66
+ }}
67
+ onError={() => {
68
+ setIsLoading(false);
69
+ props.onError?.();
70
+ }}
71
+ />
72
+ </>
73
+ );
74
+
75
+ if (props.aspectRatio) {
76
+ return (
77
+ <>
78
+ <div className={`aspect-[${props.aspectRatio}] ${props.wrapperClassName}`}>{mageData}</div>
79
+ </>
80
+ );
81
+ } else {
82
+ return mageData;
83
+ }
84
+ };
85
+
86
+ export { GeneralImage };
@@ -0,0 +1,446 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { useQueryClient } from '@tanstack/react-query';
5
+ import { useCart } from '@/core/feature/cart/hook/use.cart';
6
+ import toast from 'react-hot-toast';
7
+ import { BasketSummaryInterface } from '@/core/interface/basket.interface';
8
+ import { PromoCode } from '@/core/interface/prisma.interface';
9
+ import {
10
+ applyPendingAdds,
11
+ applyPendingQuantities,
12
+ cartLineKey,
13
+ CART_QUANTITY_DEBOUNCE_MS,
14
+ createEmptyOptimisticCart,
15
+ extractBasketFromInsertResponse,
16
+ extractInsertCartItem,
17
+ findCartLine,
18
+ applyBasketInsertResponse,
19
+ mergeInsertItemIntoCart,
20
+ patchCartCache,
21
+ patchCartItemQuantity,
22
+ readCartCache,
23
+ removeCartLineByKey,
24
+ PendingCartAdd,
25
+ } from '@/core/feature/cart/util/cart-optimistic';
26
+
27
+ interface PendingQuantityUpdate {
28
+ quantity: number;
29
+ productId?: string;
30
+ }
31
+
32
+ interface AddToCartPayload {
33
+ productId: string | number;
34
+ quantity: number;
35
+ variantId?: string | number;
36
+ linePay?: number;
37
+ type?: string;
38
+ deposit?: string;
39
+ }
40
+
41
+ export interface CartState {
42
+ cart: BasketSummaryInterface | null;
43
+ cartLoading: boolean;
44
+ isAddingToCart: boolean;
45
+ isUpdatingCart: boolean;
46
+ isProductAdding: (productId: string | number, variantId?: string | number | null) => boolean;
47
+ addToCart: (productId: string | number, quantity: number, variantId?: string | number, options?: { linePay?: number }) => void;
48
+ updateQuantity: (itemId: string | number, quantity: number, productId?: string) => void;
49
+ changeQuantity: (itemId: string | number, delta: number, productId?: string, variantId?: string | number | null) => void;
50
+ removeItem: (itemId: string | number) => void;
51
+ cartTotal: number;
52
+ promoCode: PromoCode | null;
53
+ setPromoCode: (promoCode: PromoCode | null) => void;
54
+ }
55
+
56
+ export const useCartState = (isAuthenticated: boolean): CartState => {
57
+ const queryClient = useQueryClient();
58
+ const [promoCode, setPromoCode] = useState<PromoCode | null>(null);
59
+ const [pendingQuantities, setPendingQuantities] = useState<Record<string, number>>({});
60
+ const [pendingAdds, setPendingAdds] = useState<Record<string, PendingCartAdd>>({});
61
+ const [addingKeys, setAddingKeys] = useState<Record<string, boolean>>({});
62
+
63
+ const pendingUpdatesRef = useRef<Map<string, PendingQuantityUpdate>>(new Map());
64
+ const debounceTimersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
65
+ const pendingAddsRef = useRef<Record<string, PendingCartAdd>>({});
66
+ const pendingQuantitiesRef = useRef<Record<string, number>>({});
67
+ const insertQueueRef = useRef<Promise<void>>(Promise.resolve());
68
+ const cancelledInsertKeysRef = useRef<Set<string>>(new Set());
69
+ const displayCartRef = useRef<BasketSummaryInterface | null>(null);
70
+
71
+ const cartHook = useCart();
72
+
73
+ const { data: cart, isLoading: cartLoading } = cartHook.fetch({
74
+ isEnabled: isAuthenticated,
75
+ });
76
+
77
+ const insertMutation = cartHook.insert();
78
+ const updateItemMutation = cartHook.updateItem();
79
+ const removeMutation = cartHook.remove();
80
+
81
+ const isUpdatingCart = updateItemMutation.isPending || removeMutation.isPending;
82
+ const isAddingToCart = Object.keys(addingKeys).length > 0 || insertMutation.isPending;
83
+
84
+ const serverCart = useMemo(() => readCartCache(queryClient) ?? cart, [cart, queryClient]);
85
+
86
+ const displayCart = useMemo(() => {
87
+ if (!serverCart && Object.keys(pendingAdds).length === 0) return null;
88
+
89
+ const base = serverCart ?? createEmptyOptimisticCart();
90
+
91
+ let result = base;
92
+ if (Object.keys(pendingAdds).length > 0) {
93
+ result = applyPendingAdds(result, pendingAdds);
94
+ }
95
+ if (Object.keys(pendingQuantities).length > 0) {
96
+ result = applyPendingQuantities(result, pendingQuantities);
97
+ }
98
+ return result;
99
+ }, [serverCart, pendingAdds, pendingQuantities]);
100
+
101
+ displayCartRef.current = displayCart;
102
+
103
+ useEffect(() => {
104
+ pendingAddsRef.current = pendingAdds;
105
+ }, [pendingAdds]);
106
+
107
+ useEffect(() => {
108
+ pendingQuantitiesRef.current = pendingQuantities;
109
+ }, [pendingQuantities]);
110
+
111
+ useEffect(() => {
112
+ if (!cart) return;
113
+ if (cart.promoCode?.id) {
114
+ setPromoCode((prev) => (prev?.id === cart.promoCode?.id ? prev : (cart.promoCode as PromoCode)));
115
+ return;
116
+ }
117
+ if (!cart.promoCode?.id) {
118
+ setPromoCode(null);
119
+ }
120
+ }, [cart?.promoCode?.id, cart?.promoCode?.code]);
121
+
122
+ useEffect(() => {
123
+ return () => {
124
+ debounceTimersRef.current.forEach((timer) => clearTimeout(timer));
125
+ debounceTimersRef.current.clear();
126
+ };
127
+ }, []);
128
+
129
+ const isProductAdding = (productId: string | number, variantId?: string | number | null) => {
130
+ return Boolean(addingKeys[cartLineKey(productId, variantId)]);
131
+ };
132
+
133
+ const setAdding = (key: string, value: boolean) => {
134
+ setAddingKeys((prev) => {
135
+ if (value) return { ...prev, [key]: true };
136
+ if (!(key in prev)) return prev;
137
+ const next = { ...prev };
138
+ delete next[key];
139
+ return next;
140
+ });
141
+ };
142
+
143
+ const applyLocalQuantity = (itemId: string, quantity: number) => {
144
+ setPendingQuantities((prev) => {
145
+ const next = { ...prev, [itemId]: quantity };
146
+ pendingQuantitiesRef.current = next;
147
+ return next;
148
+ });
149
+ };
150
+
151
+ const getLineQuantity = (itemId: string) => {
152
+ if (itemId in pendingQuantitiesRef.current) {
153
+ return Number(pendingQuantitiesRef.current[itemId]);
154
+ }
155
+
156
+ if (itemId.startsWith('optimistic-')) {
157
+ const key = itemId.replace('optimistic-', '');
158
+ const add = pendingAddsRef.current[key];
159
+ if (add) return Number(add.quantity);
160
+ }
161
+
162
+ const item = displayCartRef.current?.items?.find((line) => String(line.id) === itemId);
163
+ return Number(item?.quantity || 0);
164
+ };
165
+
166
+ const clearPendingQuantity = (itemId: string) => {
167
+ setPendingQuantities((prev) => {
168
+ if (!(itemId in prev)) return prev;
169
+ const next = { ...prev };
170
+ delete next[itemId];
171
+ return next;
172
+ });
173
+ pendingUpdatesRef.current.delete(itemId);
174
+
175
+ const timer = debounceTimersRef.current.get(itemId);
176
+ if (timer) {
177
+ clearTimeout(timer);
178
+ debounceTimersRef.current.delete(itemId);
179
+ }
180
+ };
181
+
182
+ const flushQuantityUpdate = (itemId: string) => {
183
+ const pending = pendingUpdatesRef.current.get(itemId);
184
+ if (!pending) return;
185
+
186
+ pendingUpdatesRef.current.delete(itemId);
187
+ debounceTimersRef.current.delete(itemId);
188
+
189
+ updateItemMutation.mutate(
190
+ { id: itemId, quantity: pending.quantity, productId: pending.productId },
191
+ {
192
+ onSuccess: (data) => {
193
+ patchCartCache(queryClient, (old) => applyBasketInsertResponse(data, old));
194
+ if (!pendingUpdatesRef.current.has(itemId)) {
195
+ setPendingQuantities((prev) => {
196
+ if (!(itemId in prev)) return prev;
197
+ const next = { ...prev };
198
+ delete next[itemId];
199
+ return next;
200
+ });
201
+ }
202
+ },
203
+ onError: (error: any) => {
204
+ queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
205
+ setPendingQuantities((prev) => {
206
+ if (!(itemId in prev)) return prev;
207
+ const next = { ...prev };
208
+ delete next[itemId];
209
+ return next;
210
+ });
211
+ toast.error(error?.response?.data?.info?.message || 'Hata oluştu');
212
+ },
213
+ }
214
+ );
215
+ };
216
+
217
+ const applyInsertSuccess = (response: unknown, key: string, productId: string | number) => {
218
+ if (cancelledInsertKeysRef.current.has(key)) {
219
+ cancelledInsertKeysRef.current.delete(key);
220
+ const insertItem = extractInsertCartItem(response);
221
+ if (insertItem?.id) {
222
+ removeMutation.mutate(insertItem.id);
223
+ }
224
+ return;
225
+ }
226
+
227
+ const pendingAdd = pendingAddsRef.current[key];
228
+ const pendingQuantity = pendingAdd?.quantity ?? 1;
229
+
230
+ const basket = extractBasketFromInsertResponse(response);
231
+ const insertItem = extractInsertCartItem(response);
232
+
233
+ if (basket) {
234
+ patchCartCache(queryClient, (old) => applyBasketInsertResponse(basket, old));
235
+ } else if (insertItem) {
236
+ patchCartCache(queryClient, (old) => {
237
+ const base = old ?? createEmptyOptimisticCart();
238
+ return mergeInsertItemIntoCart(
239
+ base,
240
+ insertItem,
241
+ pendingAdd ? { ...pendingAdd, quantity: pendingQuantity } : undefined
242
+ );
243
+ });
244
+ } else {
245
+ queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
246
+ }
247
+
248
+ if (insertItem || basket) {
249
+ const updatedCart = readCartCache(queryClient);
250
+ if (findCartLine(updatedCart, productId, pendingAdd?.variantId)) {
251
+ setPendingAdds((prev) => {
252
+ if (!(key in prev)) return prev;
253
+ const next = { ...prev };
254
+ delete next[key];
255
+ pendingAddsRef.current = next;
256
+ return next;
257
+ });
258
+ }
259
+ }
260
+
261
+ if (insertItem && pendingQuantity > insertItem.quantity) {
262
+ updateItemMutation.mutate(
263
+ { id: insertItem.id, quantity: pendingQuantity, productId: String(productId) },
264
+ {
265
+ onSuccess: (data) => {
266
+ patchCartCache(queryClient, (old) => applyBasketInsertResponse(data, old));
267
+ },
268
+ }
269
+ );
270
+ }
271
+ };
272
+
273
+ const enqueueInsert = (payload: AddToCartPayload, key: string) => {
274
+ setAdding(key, true);
275
+
276
+ insertQueueRef.current = insertQueueRef.current
277
+ .then(() => insertMutation.mutateAsync(payload))
278
+ .then((response) => {
279
+ applyInsertSuccess(response, key, payload.productId);
280
+ })
281
+ .catch((error: any) => {
282
+ setPendingAdds((prev) => {
283
+ if (!(key in prev)) return prev;
284
+ const next = { ...prev };
285
+ delete next[key];
286
+ pendingAddsRef.current = next;
287
+ return next;
288
+ });
289
+ patchCartCache(queryClient, (old) => {
290
+ const base = old ?? createEmptyOptimisticCart();
291
+ return removeCartLineByKey(base, key);
292
+ });
293
+ queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
294
+ toast.error(error?.response?.data?.info?.message || 'Sepete eklenirken bir hata oluştu');
295
+ })
296
+ .finally(() => {
297
+ setAdding(key, false);
298
+ });
299
+ };
300
+
301
+ const addToCart = (
302
+ productId: string | number,
303
+ quantity: number,
304
+ variantId?: string | number,
305
+ options?: { linePay?: number }
306
+ ) => {
307
+ const key = cartLineKey(productId, variantId);
308
+ const existing = findCartLine(displayCart, productId, variantId);
309
+
310
+ if (existing) {
311
+ changeQuantity(existing.id, quantity, String(productId), variantId);
312
+ return;
313
+ }
314
+
315
+ const pendingAdd: PendingCartAdd = {
316
+ productId: String(productId),
317
+ variantId: variantId != null ? String(variantId) : undefined,
318
+ quantity,
319
+ linePay: options?.linePay,
320
+ };
321
+
322
+ const nextPending = { ...pendingAddsRef.current, [key]: pendingAdd };
323
+ pendingAddsRef.current = nextPending;
324
+ setPendingAdds(nextPending);
325
+
326
+ patchCartCache(queryClient, (old) => {
327
+ const base = old ?? createEmptyOptimisticCart();
328
+ return applyPendingAdds(base, { [key]: pendingAdd });
329
+ });
330
+
331
+ enqueueInsert(
332
+ {
333
+ productId,
334
+ quantity,
335
+ variantId,
336
+ type: 'increment',
337
+ deposit: 'disable',
338
+ linePay: options?.linePay,
339
+ },
340
+ key
341
+ );
342
+ };
343
+
344
+ const updateQuantity = (
345
+ itemId: string | number,
346
+ quantity: number,
347
+ productId?: string,
348
+ variantId?: string | number | null
349
+ ) => {
350
+ const id = itemId.toString();
351
+ if (quantity < 1) return;
352
+
353
+ if (id.startsWith('optimistic-')) {
354
+ const key = id.replace('optimistic-', '');
355
+ const currentAdd = pendingAddsRef.current[key];
356
+ if (!currentAdd) return;
357
+
358
+ const nextAdd = { ...currentAdd, quantity };
359
+ const nextPending = { ...pendingAddsRef.current, [key]: nextAdd };
360
+ pendingAddsRef.current = nextPending;
361
+ setPendingAdds(nextPending);
362
+
363
+ patchCartCache(queryClient, (old) => {
364
+ const base = old ?? createEmptyOptimisticCart();
365
+ return applyPendingAdds(base, { [key]: nextAdd });
366
+ });
367
+ return;
368
+ }
369
+
370
+ applyLocalQuantity(id, quantity);
371
+
372
+ patchCartCache(queryClient, (old) => {
373
+ const base = old ?? createEmptyOptimisticCart();
374
+ return patchCartItemQuantity(base, id, quantity, productId, variantId);
375
+ });
376
+
377
+ pendingUpdatesRef.current.set(id, { quantity, productId });
378
+
379
+ const existingTimer = debounceTimersRef.current.get(id);
380
+ if (existingTimer) clearTimeout(existingTimer);
381
+
382
+ debounceTimersRef.current.set(
383
+ id,
384
+ setTimeout(() => flushQuantityUpdate(id), CART_QUANTITY_DEBOUNCE_MS)
385
+ );
386
+ };
387
+
388
+ const changeQuantity = (
389
+ itemId: string | number,
390
+ delta: number,
391
+ productId?: string,
392
+ variantId?: string | number | null
393
+ ) => {
394
+ const id = itemId.toString();
395
+ const nextQuantity = getLineQuantity(id) + delta;
396
+
397
+ if (nextQuantity < 1) {
398
+ removeItem(id);
399
+ return;
400
+ }
401
+
402
+ updateQuantity(id, nextQuantity, productId, variantId);
403
+ };
404
+
405
+ const removeItem = (itemId: string | number) => {
406
+ const id = itemId.toString();
407
+ clearPendingQuantity(id);
408
+
409
+ if (id.startsWith('optimistic-')) {
410
+ const key = id.replace('optimistic-', '');
411
+ cancelledInsertKeysRef.current.add(key);
412
+ setPendingAdds((prev) => {
413
+ if (!(key in prev)) return prev;
414
+ const next = { ...prev };
415
+ delete next[key];
416
+ return next;
417
+ });
418
+ setAdding(key, false);
419
+ return;
420
+ }
421
+
422
+ removeMutation.mutate(id, {
423
+ onSuccess: () => {
424
+ toast.success('Ürün sepetten çıkarıldı');
425
+ },
426
+ onError: (error: any) => {
427
+ toast.error(error?.response?.data?.info?.message || 'Silinirken hata oluştu');
428
+ },
429
+ });
430
+ };
431
+
432
+ return {
433
+ cart: displayCart,
434
+ cartLoading,
435
+ isAddingToCart,
436
+ isUpdatingCart,
437
+ isProductAdding,
438
+ addToCart,
439
+ updateQuantity,
440
+ changeQuantity,
441
+ removeItem,
442
+ cartTotal: displayCart?.info?.payPrice?.pay || 0,
443
+ promoCode,
444
+ setPromoCode,
445
+ };
446
+ };
@@ -0,0 +1,151 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { useRouter, usePathname } from 'next/navigation';
5
+ import { LoginType, User } from '@/core/interface/prisma.interface';
6
+ import { useAuth } from '@/core/feature/auth/hook/use.auth';
7
+ import { useProfile } from '@/core/feature/profile/hook/use.profile';
8
+
9
+ export interface SecurityState {
10
+ profile: User | null | undefined;
11
+ authLoading: boolean;
12
+ isAuthenticated: boolean;
13
+ isGuest: boolean;
14
+ }
15
+
16
+ export const useSecurityState = (): SecurityState => {
17
+ const router = useRouter();
18
+ const pathname = usePathname();
19
+
20
+ const [authLoading, setAuthLoading] = useState<boolean>(true);
21
+ const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
22
+ const [isAuthEstablished, setIsAuthEstablished] = useState<boolean>(false);
23
+
24
+ const { mutate: tokenCheck } = useAuth().token();
25
+ const { mutate: loginGuest } = useAuth().loginGuest();
26
+
27
+ const profileQuery = useProfile().fetch({ isEnabled: isAuthEstablished && isAuthenticated });
28
+ const profile = profileQuery.data;
29
+
30
+ const isGuest = useMemo(() => {
31
+ return profile?.loginType === LoginType.GUEST;
32
+ }, [profile]);
33
+
34
+ const isInvalidTokenError = (error: any) => {
35
+ const status = error?.response?.status;
36
+ return status === 401 || status === 403;
37
+ };
38
+
39
+ const isServerAccessError = (error: any) => {
40
+ const status = error?.response?.status;
41
+ return !error?.response || (typeof status === 'number' && status >= 500);
42
+ };
43
+
44
+ const ensureGuestLogin = useCallback(() => {
45
+ loginGuest(undefined, {
46
+ onSuccess: () => {
47
+ setIsAuthEstablished(true);
48
+ setIsAuthenticated(true);
49
+ setAuthLoading(false);
50
+ },
51
+ onError: () => {
52
+ setIsAuthEstablished(false);
53
+ setIsAuthenticated(false);
54
+ setAuthLoading(false);
55
+ },
56
+ });
57
+ }, [loginGuest]);
58
+
59
+ const checkToken = useCallback(() => {
60
+ if (typeof window !== 'undefined') {
61
+ const token = localStorage.getItem('koksal-token');
62
+ if (token && token.trim() !== '' && token.length > 3) {
63
+ setAuthLoading(true);
64
+ tokenCheck(undefined, {
65
+ onSuccess: () => {
66
+ setIsAuthEstablished(true);
67
+ setIsAuthenticated(true);
68
+ setAuthLoading(false);
69
+ },
70
+ onError: (error: any) => {
71
+ if (isServerAccessError(error)) {
72
+ setIsAuthEstablished(true);
73
+ setIsAuthenticated(true);
74
+ setAuthLoading(false);
75
+ return;
76
+ }
77
+
78
+ if (isInvalidTokenError(error)) {
79
+ localStorage.removeItem('koksal-token');
80
+ ensureGuestLogin();
81
+ return;
82
+ }
83
+
84
+ ensureGuestLogin();
85
+ },
86
+ });
87
+ } else {
88
+ setAuthLoading(true);
89
+ ensureGuestLogin();
90
+ }
91
+ }
92
+ }, [tokenCheck, ensureGuestLogin]);
93
+
94
+ useEffect(() => {
95
+ checkToken();
96
+ }, [checkToken]);
97
+
98
+ useEffect(() => {
99
+ if (typeof window !== 'undefined') {
100
+ const handleStorageChange = (e: StorageEvent) => {
101
+ if (e.key === 'koksal-token') {
102
+ checkToken();
103
+ }
104
+ };
105
+
106
+ const handleCustomStorageChange = () => {
107
+ checkToken();
108
+ };
109
+
110
+ window.addEventListener('storage', handleStorageChange);
111
+ window.addEventListener('koksal-token-changed', handleCustomStorageChange);
112
+
113
+ return () => {
114
+ window.removeEventListener('storage', handleStorageChange);
115
+ window.removeEventListener('koksal-token-changed', handleCustomStorageChange);
116
+ };
117
+ }
118
+ }, [checkToken]);
119
+
120
+ useEffect(() => {
121
+ const isLoading =
122
+ authLoading || (isAuthEstablished && (profileQuery.isLoading || profileQuery.isFetching));
123
+ if (isLoading) return;
124
+
125
+ if (pathname.startsWith('/guvenlik') && isAuthenticated && profile?.loginType !== LoginType.GUEST) {
126
+ router.push('/hesabim');
127
+ }
128
+ if (pathname.startsWith('/hesabim') && (!isAuthenticated || profile?.loginType === LoginType.GUEST)) {
129
+ router.push('/guvenlik/giris-yap');
130
+ }
131
+ }, [
132
+ pathname,
133
+ isAuthenticated,
134
+ authLoading,
135
+ isAuthEstablished,
136
+ profileQuery.isLoading,
137
+ profileQuery.isFetching,
138
+ router,
139
+ profile?.loginType,
140
+ ]);
141
+
142
+ const resolvedAuthLoading =
143
+ authLoading || (isAuthEstablished && (profileQuery.isLoading || profileQuery.isFetching));
144
+
145
+ return {
146
+ profile,
147
+ authLoading: resolvedAuthLoading,
148
+ isAuthenticated,
149
+ isGuest,
150
+ };
151
+ };