@raxonltd/raxon-core 1.1.7 → 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,1964 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { CheckCircle2, HelpCircle, Loader2, Package, XCircle } from 'lucide-react';
6
+ import { useState, useRef, useEffect, useMemo, useCallback, Suspense, createContext, useContext } from 'react';
7
+ import { useQueryClient } from '@tanstack/react-query';
8
+ import { useForm } from 'react-hook-form';
9
+ import { Input } from 'rizzui/input';
10
+ import { Button } from 'rizzui/button';
11
+ import { useRaxon } from '@/core/raxon.context';
12
+ import { LOGIN_SUCCESS_EVENT } from '@/core/feature/auth/hook/use.auth';
13
+ import { ModalAuth, ModalAuthRef } from '@/core/feature/auth/modal/modal.auth';
14
+ import { BasketAddressInput, useCart } from '@/core/feature/cart/hook/use.cart';
15
+ import { useAddress } from '@/core/feature/address/hook/use.addres';
16
+ import { useAddressAutocomplete } from '@/core/feature/address/hook/use.address-autocomplete';
17
+ import { AddressSearchInput } from '@/core/feature/address/form/address-search-input';
18
+ import { formatAddressSummary, parsedAddressToFormFields } from '@/core/feature/address/util/parse-google-place';
19
+ import { formatBasketItemVariantLine } from '@/core/util/basket.item.display';
20
+ import { GeneralImage } from '@/core/component/general.image';
21
+ import { Address, AddressType, BankAccount, DeliveryMethod, PaymentMethod, PaymentProvider, PaymentTerms, Status } from '@/core/interface/prisma.interface';
22
+ import { consumeGarantiPaymentHtml, storeGarantiPaymentHtml, submitGarantiPaymentHtml } from '@/core/util/garanti-payment';
23
+ import { useOrder } from '@/core/feature/order/hook/use.order';
24
+ import { nexineAxios } from '@/core/util/nexine.axios';
25
+ import { resolveClientIp } from '@/core/util/client-ip';
26
+ import { CartPromoCodeSection } from '@/core/feature/cart/component/cart.promo.code.section';
27
+
28
+ interface AddressFormData {
29
+ firstName: string;
30
+ lastName: string;
31
+ phone: string;
32
+ street: string;
33
+ buildingNumber: string;
34
+ apartmentNumber: string;
35
+ postalCode: string;
36
+ province: string;
37
+ district: string;
38
+ newsletter: boolean;
39
+ billingAddressDifferent: boolean;
40
+ billingFullName: string;
41
+ billingTaxNumber: string;
42
+ billingProvince: string;
43
+ billingDistrict: string;
44
+ billingStreet: string;
45
+ savedDeliveryAddressId: string;
46
+ savedBillingAddressId: string;
47
+ addressSearch: string;
48
+ }
49
+
50
+ const addressFormDefaultValues: AddressFormData = {
51
+ firstName: '',
52
+ lastName: '',
53
+ phone: '',
54
+ street: '',
55
+ buildingNumber: '',
56
+ apartmentNumber: '',
57
+ postalCode: '',
58
+ province: '',
59
+ district: '',
60
+ newsletter: false,
61
+ billingAddressDifferent: false,
62
+ billingFullName: '',
63
+ billingTaxNumber: '',
64
+ billingProvince: '',
65
+ billingDistrict: '',
66
+ billingStreet: '',
67
+ savedDeliveryAddressId: '',
68
+ savedBillingAddressId: '',
69
+ addressSearch: '',
70
+ };
71
+
72
+ type CheckoutStep = 'contact' | 'address' | 'checkout';
73
+
74
+ export interface CheckoutViewProps {
75
+ webReturnUrl?: string;
76
+ }
77
+
78
+ type CheckoutViewConfig = {
79
+ webReturnUrl: string;
80
+ };
81
+
82
+ const CheckoutViewContext = createContext<CheckoutViewConfig>({
83
+ webReturnUrl: '/sepet/odeme',
84
+ });
85
+
86
+ function useCheckoutViewConfig() {
87
+ return useContext(CheckoutViewContext);
88
+ }
89
+
90
+ function resolveAbsoluteReturnUrl(webReturnUrl: string): string {
91
+ if (/^https?:\/\//i.test(webReturnUrl)) return webReturnUrl;
92
+ if (typeof window === 'undefined') return webReturnUrl;
93
+ return `${window.location.origin}${webReturnUrl.startsWith('/') ? webReturnUrl : `/${webReturnUrl}`}`;
94
+ }
95
+
96
+ function withCheckoutQuery(webReturnUrl: string, query: string): string {
97
+ const separator = webReturnUrl.includes('?') ? '&' : '?';
98
+ return `${webReturnUrl}${separator}${query}`;
99
+ }
100
+
101
+ function formatAddressLine(addr: {
102
+ firstName?: string | null;
103
+ lastName?: string | null;
104
+ fullAddress?: string | null;
105
+ streetName?: string | null;
106
+ administrativeAreaLevel2?: string | null;
107
+ administrativeAreaLevel1?: string | null;
108
+ postalCode?: string | null;
109
+ companyName?: string | null;
110
+ taxNumber?: string | null;
111
+ }) {
112
+ const name = addr.companyName || `${addr.firstName ?? ''} ${addr.lastName ?? ''}`.trim();
113
+ const street = addr.streetName ?? addr.fullAddress ?? '';
114
+ const location = [addr.administrativeAreaLevel2, addr.administrativeAreaLevel1].filter(Boolean).join(', ');
115
+ return { name, street, location, postalCode: addr.postalCode, taxNumber: addr.taxNumber };
116
+ }
117
+
118
+ function pickDefaultDeliveryAddress(addresses: Address[]): Address | undefined {
119
+ return addresses.find(addr => addr.type === AddressType.DELIVERY) ?? addresses[0];
120
+ }
121
+
122
+ function ViewStep1({ onContinue }: { onContinue: (email: string) => void }) {
123
+ const { cart, profile, isAuthenticated, isGuest } = useRaxon();
124
+ const modalAuthRef = useRef<ModalAuthRef>(null);
125
+
126
+ const [emailInput, setEmailInput] = useState('');
127
+ const [emailError, setEmailError] = useState<string | null>(null);
128
+ const [isSaving, setIsSaving] = useState(false);
129
+ const lastSavedEmailRef = useRef('');
130
+ const saveInFlightRef = useRef<Promise<boolean> | null>(null);
131
+ const userEditedEmailRef = useRef(false);
132
+
133
+ const updateBasketMutation = useCart().update();
134
+
135
+ useEffect(() => {
136
+ const fromCart = cart?.email?.trim() || '';
137
+ const fromProfile = isAuthenticated && !isGuest ? profile?.email?.trim() || '' : '';
138
+ const resolved = fromCart || fromProfile;
139
+
140
+ if (fromCart) {
141
+ lastSavedEmailRef.current = fromCart;
142
+ }
143
+
144
+ if (!resolved || userEditedEmailRef.current) return;
145
+
146
+ setEmailInput(resolved);
147
+ }, [cart?.email, profile?.email, isAuthenticated, isGuest]);
148
+
149
+ const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
150
+
151
+ const saveEmail = (email: string): Promise<boolean> => {
152
+ if (saveInFlightRef.current) return saveInFlightRef.current;
153
+
154
+ const promise = (async () => {
155
+ const trimmed = email.trim();
156
+ if (!trimmed) {
157
+ setEmailError('E-posta adresi zorunludur.');
158
+ return false;
159
+ }
160
+ if (!isValidEmail(trimmed)) {
161
+ setEmailError('Geçerli bir e-posta adresi giriniz.');
162
+ return false;
163
+ }
164
+ if (trimmed === lastSavedEmailRef.current) return true;
165
+ if (!cart?.id) {
166
+ setEmailError('Sepet bulunamadı. Lütfen sayfayı yenileyin.');
167
+ return false;
168
+ }
169
+
170
+ setEmailError(null);
171
+ setIsSaving(true);
172
+ try {
173
+ await updateBasketMutation.mutateAsync({ id: cart.id, email: trimmed });
174
+ lastSavedEmailRef.current = trimmed;
175
+ return true;
176
+ } catch {
177
+ setEmailError('E-posta kaydedilemedi. Lütfen tekrar deneyin.');
178
+ return false;
179
+ } finally {
180
+ setIsSaving(false);
181
+ }
182
+ })();
183
+
184
+ saveInFlightRef.current = promise;
185
+ void promise.finally(() => {
186
+ if (saveInFlightRef.current === promise) saveInFlightRef.current = null;
187
+ });
188
+ return promise;
189
+ };
190
+
191
+ const handleEmailBlur = () => {
192
+ void saveEmail(emailInput);
193
+ };
194
+
195
+ return (
196
+ <>
197
+ <ModalAuth ref={modalAuthRef} />
198
+ <div className="space-y-4">
199
+ <div>
200
+ <h2 className="text-base font-semibold text-gray-900">İletişim</h2>
201
+ <p className="mt-1 text-sm text-gray-500">Sipariş onayı ve kargo bilgileri bu adrese gönderilir.</p>
202
+ </div>
203
+
204
+ {!isGuest && isAuthenticated ? (
205
+ <p className="text-sm text-gray-600">
206
+ <button
207
+ type="button"
208
+ className="font-medium text-[#CF0A2C] hover:underline"
209
+ onClick={() => modalAuthRef.current?.open('login')}
210
+ >
211
+ Hesabınızla giriş yaptınız
212
+ </button>
213
+ </p>
214
+ ) : (
215
+ <p className="text-sm text-gray-600">
216
+ Hesabınız var mı?
217
+ <button
218
+ type="button"
219
+ className="font-medium text-[#CF0A2C] hover:underline"
220
+ onClick={() => modalAuthRef.current?.open('login')}
221
+ >
222
+ Giriş yap
223
+ </button>
224
+ {' veya '}
225
+ <button
226
+ type="button"
227
+ className="font-medium text-[#CF0A2C] hover:underline"
228
+ onClick={() => modalAuthRef.current?.open('register')}
229
+ >
230
+ kayıt ol
231
+ </button>
232
+ </p>
233
+ )}
234
+
235
+ <Input
236
+ type="email"
237
+ label="E-posta"
238
+ placeholder="ornek@email.com"
239
+ value={emailInput}
240
+ onChange={e => {
241
+ userEditedEmailRef.current = true;
242
+ setEmailInput(e.target.value);
243
+ }}
244
+ onBlur={handleEmailBlur}
245
+ size="lg"
246
+ className="rounded-xl"
247
+ required
248
+ />
249
+ {isSaving ? <p className="text-xs text-gray-500">Kaydediliyor…</p> : null}
250
+ {emailError ? <p className="text-sm text-red-600">{emailError}</p> : null}
251
+
252
+ <Button
253
+ type="button"
254
+ size="lg"
255
+ className="w-full rounded-xl bg-[#CF0A2C] font-semibold text-white hover:bg-[#a80824]"
256
+ disabled={!emailInput.trim() || isSaving}
257
+ onClick={async () => {
258
+ const ok = await saveEmail(emailInput);
259
+ if (ok) onContinue(emailInput.trim());
260
+ }}
261
+ >
262
+ Devam et
263
+ </Button>
264
+ </div>
265
+ </>
266
+ );
267
+ }
268
+
269
+ function ViewStep2({ onComplete, onBack }: { onComplete: () => void; onBack: () => void }) {
270
+ const { cart } = useRaxon();
271
+ const deliveryFormHydratedKeyRef = useRef<string | null>(null);
272
+ const addressBasketSyncRef = useRef<string | null>(null);
273
+
274
+ const {
275
+ query: addressSearchQuery,
276
+ setQuery: setAddressSearchQuery,
277
+ onUserQueryChange: onAddressSearchQueryChange,
278
+ results: addressSearchResults,
279
+ isSearching: isAddressSearching,
280
+ showResults: showAddressSearchResults,
281
+ setShowResults: setShowAddressSearchResults,
282
+ selectPlace: selectAddressPlace,
283
+ resetSearch: resetAddressSearch,
284
+ } = useAddressAutocomplete();
285
+
286
+ const updateBasketMutation = useCart().update();
287
+ const { data: addressesPayload } = useAddress().fetch();
288
+
289
+ const savedAddresses: Address[] = addressesPayload?.data ?? [];
290
+
291
+ const addressForm = useForm<AddressFormData>({
292
+ defaultValues: addressFormDefaultValues,
293
+ });
294
+
295
+ const {
296
+ register,
297
+ handleSubmit,
298
+ watch,
299
+ setValue,
300
+ getValues,
301
+ reset,
302
+ setError,
303
+ clearErrors,
304
+ formState: { errors, isSubmitting },
305
+ } = addressForm;
306
+
307
+ const billingAddressDifferent = watch('billingAddressDifferent');
308
+ const savedDeliveryAddressId = watch('savedDeliveryAddressId');
309
+ const savedBillingAddressId = watch('savedBillingAddressId');
310
+ const provinceField = register('province', { required: 'İl giriniz' });
311
+ const districtField = register('district', { required: 'İlçe giriniz' });
312
+ const billingProvinceField = register('billingProvince', {
313
+ required: billingAddressDifferent ? 'İl giriniz' : false,
314
+ });
315
+ const billingDistrictField = register('billingDistrict', {
316
+ required: billingAddressDifferent ? 'İlçe giriniz' : false,
317
+ });
318
+
319
+ useEffect(() => {
320
+ const d = cart?.deliveryAddress;
321
+ if (!cart || !d?.id) return;
322
+ const hydrateKey = `${cart.id}:${d.id}`;
323
+ if (deliveryFormHydratedKeyRef.current === hydrateKey) return;
324
+ deliveryFormHydratedKeyRef.current = hydrateKey;
325
+ reset({
326
+ ...addressFormDefaultValues,
327
+ firstName: d.firstName ?? '',
328
+ lastName: d.lastName ?? '',
329
+ phone: d.phoneNumber ?? '',
330
+ street: d.streetName ?? d.fullAddress ?? '',
331
+ buildingNumber: d.buildingName ?? '',
332
+ apartmentNumber: d.apartmentNumber ?? '',
333
+ postalCode: d.postalCode ?? '',
334
+ province: d.administrativeAreaLevel1 ?? '',
335
+ district: d.administrativeAreaLevel2 ?? '',
336
+ savedDeliveryAddressId: d.id,
337
+ });
338
+ setAddressSearchQuery(formatAddressSummary(d));
339
+ }, [cart, cart?.deliveryAddress?.id, reset, setAddressSearchQuery]);
340
+
341
+ useEffect(() => {
342
+ if (cart?.deliveryAddress?.id) return;
343
+ if (!addressesPayload?.data?.length) return;
344
+
345
+ const defaultAddress = pickDefaultDeliveryAddress(addressesPayload.data);
346
+ if (!defaultAddress) return;
347
+
348
+ const hydrateKey = `saved:${defaultAddress.id}`;
349
+ if (deliveryFormHydratedKeyRef.current === hydrateKey) return;
350
+ deliveryFormHydratedKeyRef.current = hydrateKey;
351
+
352
+ reset({
353
+ ...addressFormDefaultValues,
354
+ firstName: defaultAddress.firstName ?? '',
355
+ lastName: defaultAddress.lastName ?? '',
356
+ phone: defaultAddress.phoneNumber ?? '',
357
+ street: defaultAddress.streetName ?? defaultAddress.fullAddress ?? '',
358
+ buildingNumber: defaultAddress.buildingName ?? '',
359
+ apartmentNumber: defaultAddress.apartmentNumber ?? '',
360
+ postalCode: defaultAddress.postalCode ?? '',
361
+ province: defaultAddress.administrativeAreaLevel1 ?? '',
362
+ district: defaultAddress.administrativeAreaLevel2 ?? '',
363
+ savedDeliveryAddressId: defaultAddress.id,
364
+ });
365
+ setAddressSearchQuery(formatAddressSummary(defaultAddress));
366
+ }, [addressesPayload?.data, cart?.deliveryAddress?.id, reset, setAddressSearchQuery]);
367
+
368
+ useEffect(() => {
369
+ if (updateBasketMutation.isPending) return;
370
+ if (!cart?.id || cart?.deliveryAddress?.id) return;
371
+ if (!addressesPayload?.data?.length) return;
372
+
373
+ const defaultAddress = pickDefaultDeliveryAddress(addressesPayload.data);
374
+ if (!defaultAddress?.id) return;
375
+
376
+ const syncKey = `${cart.id}:${defaultAddress.id}`;
377
+ if (addressBasketSyncRef.current === syncKey) return;
378
+ addressBasketSyncRef.current = syncKey;
379
+
380
+ updateBasketMutation.mutate({
381
+ id: cart.id,
382
+ deliveryAddressId: defaultAddress.id,
383
+ invoiceAddressId: defaultAddress.id,
384
+ });
385
+ }, [cart?.id, cart?.deliveryAddress?.id, addressesPayload?.data, updateBasketMutation.isPending, updateBasketMutation.mutate]);
386
+
387
+ useEffect(() => {
388
+ const invoiceId = cart?.invoiceAddress?.id;
389
+ const deliveryId = cart?.deliveryAddress?.id;
390
+ if (!invoiceId || !deliveryId) return;
391
+ if (invoiceId === deliveryId) return;
392
+
393
+ setValue('billingAddressDifferent', true);
394
+
395
+ const matched = savedAddresses.find(a => a.id === invoiceId);
396
+ if (matched) {
397
+ setValue('savedBillingAddressId', matched.id);
398
+ return;
399
+ }
400
+
401
+ const inv = cart.invoiceAddress!;
402
+ setValue('billingFullName', inv.companyName ?? `${inv.firstName ?? ''} ${inv.lastName ?? ''}`.trim());
403
+ setValue('billingTaxNumber', inv.taxNumber ?? '');
404
+ setValue('billingProvince', inv.administrativeAreaLevel1 ?? '');
405
+ setValue('billingDistrict', inv.administrativeAreaLevel2 ?? '');
406
+ setValue('billingStreet', (inv as unknown as Address).streetName ?? inv.description ?? '');
407
+ }, [cart?.invoiceAddress?.id, cart?.deliveryAddress?.id, savedAddresses, setValue]);
408
+
409
+ const applyParsedAddressToForm = (fields: ReturnType<typeof parsedAddressToFormFields>) => {
410
+ setValue('street', fields.street);
411
+ setValue('buildingNumber', fields.buildingNumber);
412
+ setValue('apartmentNumber', fields.apartmentNumber);
413
+ setValue('postalCode', fields.postalCode);
414
+ setValue('province', fields.provinceName);
415
+ setValue('district', fields.districtName);
416
+ };
417
+
418
+ const handleSelectGoogleAddress = async (placeId: string) => {
419
+ clearErrors('addressSearch');
420
+ const prediction = addressSearchResults.find(r => r.place_id === placeId);
421
+ if (!prediction) return;
422
+
423
+ const parsed = await selectAddressPlace(prediction);
424
+ if (!parsed) {
425
+ setError('addressSearch', { type: 'manual', message: 'Adres bilgileri alınamadı. Lütfen tekrar deneyin.' });
426
+ return;
427
+ }
428
+
429
+ applyParsedAddressToForm(parsedAddressToFormFields(parsed));
430
+ setValue('savedDeliveryAddressId', '');
431
+ };
432
+
433
+ const handleAddressSearchQueryChange = (value: string) => {
434
+ onAddressSearchQueryChange(value);
435
+ clearErrors('addressSearch');
436
+
437
+ if (!value.trim()) {
438
+ resetAddressSearch();
439
+ setValue('street', '');
440
+ setValue('buildingNumber', '');
441
+ setValue('apartmentNumber', '');
442
+ setValue('postalCode', '');
443
+ setValue('province', '');
444
+ setValue('district', '');
445
+ }
446
+ };
447
+
448
+ const handleSelectSavedAddress = (addr: Address) => {
449
+ reset({
450
+ ...getValues(),
451
+ firstName: addr.firstName ?? '',
452
+ lastName: addr.lastName ?? '',
453
+ phone: addr.phoneNumber ?? '',
454
+ street: addr.streetName ?? addr.fullAddress ?? '',
455
+ buildingNumber: addr.buildingName ?? '',
456
+ apartmentNumber: addr.apartmentNumber ?? '',
457
+ postalCode: addr.postalCode ?? '',
458
+ province: addr.administrativeAreaLevel1 ?? '',
459
+ district: addr.administrativeAreaLevel2 ?? '',
460
+ savedDeliveryAddressId: addr.id,
461
+ });
462
+ setAddressSearchQuery(formatAddressSummary(addr));
463
+ };
464
+
465
+ const handleSelectBillingAddress = (addr: Address) => {
466
+ setValue('savedBillingAddressId', addr.id);
467
+ setValue('billingFullName', addr.companyName ?? `${addr.firstName ?? ''} ${addr.lastName ?? ''}`.trim());
468
+ setValue('billingTaxNumber', addr.taxNumber ?? '');
469
+ setValue('billingProvince', addr.administrativeAreaLevel1 ?? '');
470
+ setValue('billingDistrict', addr.administrativeAreaLevel2 ?? '');
471
+ setValue('billingStreet', addr.streetName ?? addr.fullAddress ?? '');
472
+ };
473
+
474
+ const buildDeliveryAddress = (data: AddressFormData): BasketAddressInput => {
475
+ const fullAddr = [data.street, data.buildingNumber, data.apartmentNumber].filter(Boolean).join(', ');
476
+ const phoneDigits = data.phone.replace(/\D/g, '').replace(/^90/, '');
477
+
478
+ return {
479
+ title: 'Teslimat',
480
+ type: AddressType.DELIVERY,
481
+ firstName: data.firstName,
482
+ lastName: data.lastName,
483
+ phoneNumber: phoneDigits || data.phone,
484
+ postalCode: data.postalCode,
485
+ administrativeAreaLevel1: data.province.trim(),
486
+ administrativeAreaLevel2: data.district.trim(),
487
+ administrativeAreaLevel3: '',
488
+ fullAddress: fullAddr,
489
+ streetName: data.street,
490
+ buildingName: data.buildingNumber || null,
491
+ apartmentNumber: data.apartmentNumber || null,
492
+ country: 'Türkiye',
493
+ countryCode: 'TR',
494
+ };
495
+ };
496
+
497
+ const buildInvoiceAddress = (data: AddressFormData): BasketAddressInput => {
498
+ const phoneDigits = data.phone.replace(/\D/g, '').replace(/^90/, '');
499
+
500
+ return {
501
+ title: 'Fatura',
502
+ type: AddressType.INVOICE,
503
+ companyName: data.billingFullName,
504
+ taxNumber: data.billingTaxNumber,
505
+ phoneNumber: phoneDigits || data.phone,
506
+ postalCode: data.postalCode,
507
+ administrativeAreaLevel1: data.billingProvince.trim(),
508
+ administrativeAreaLevel2: data.billingDistrict.trim(),
509
+ administrativeAreaLevel3: '',
510
+ fullAddress: data.billingStreet,
511
+ streetName: data.billingStreet,
512
+ country: 'Türkiye',
513
+ countryCode: 'TR',
514
+ };
515
+ };
516
+
517
+ const handleAddressSubmit = async (data: AddressFormData) => {
518
+ if (!cart?.id) return;
519
+ clearErrors('root');
520
+ try {
521
+ const deliveryAddressPayload = buildDeliveryAddress(data);
522
+ const useDeliveryId = data.savedDeliveryAddressId || null;
523
+ const useInvoiceId = data.billingAddressDifferent ? data.savedBillingAddressId || null : useDeliveryId;
524
+
525
+ const basketPayload: Parameters<typeof updateBasketMutation.mutateAsync>[0] = { id: cart.id };
526
+
527
+ if (useDeliveryId) {
528
+ basketPayload.deliveryAddressId = useDeliveryId;
529
+ } else {
530
+ basketPayload.deliveryAddress = deliveryAddressPayload;
531
+ }
532
+
533
+ if (data.billingAddressDifferent) {
534
+ if (useInvoiceId) {
535
+ basketPayload.invoiceAddressId = useInvoiceId;
536
+ } else {
537
+ basketPayload.invoiceAddress = buildInvoiceAddress(data);
538
+ }
539
+ } else if (useDeliveryId) {
540
+ basketPayload.invoiceAddressId = useDeliveryId;
541
+ } else {
542
+ basketPayload.invoiceAddress = deliveryAddressPayload;
543
+ }
544
+
545
+ await updateBasketMutation.mutateAsync(basketPayload);
546
+ onComplete();
547
+ } catch {
548
+ setError('root', { type: 'server', message: 'Adres kaydedilemedi. Lütfen bilgileri kontrol edip tekrar deneyin.' });
549
+ }
550
+ };
551
+
552
+ return (
553
+ <form onSubmit={handleSubmit(handleAddressSubmit)} className="space-y-6">
554
+ {savedAddresses.length > 0 && (
555
+ <div>
556
+ <label className="rizzui-input-label block text-sm mb-1.5 font-medium">Önerilen teslimat adresi</label>
557
+ <select
558
+ value={savedDeliveryAddressId || (cart?.deliveryAddress?.id ?? '')}
559
+ onChange={e => {
560
+ if (!e.target.value) {
561
+ setValue('savedDeliveryAddressId', '');
562
+ return;
563
+ }
564
+ const addr = savedAddresses.find(a => a.id === e.target.value);
565
+ if (addr) handleSelectSavedAddress(addr);
566
+ }}
567
+ className="rizzui-input-container peer flex h-10 w-full items-center rounded-lg border border-gray-200 bg-white px-3.5 py-2 text-sm transition-all focus:border-[#CF0A2C] focus:outline-none focus:ring-2 focus:ring-[#CF0A2C]/25"
568
+ >
569
+ <option value="">Yeni adres girin</option>
570
+ {savedAddresses.map(addr => (
571
+ <option key={addr.id} value={addr.id}>
572
+ {`${addr.firstName ?? ''} ${addr.lastName ?? ''}`.trim()}
573
+ {addr.administrativeAreaLevel2 ? ` — ${addr.administrativeAreaLevel2}, ${addr.administrativeAreaLevel1}` : ''}
574
+ </option>
575
+ ))}
576
+ </select>
577
+ </div>
578
+ )}
579
+
580
+ <div>
581
+ <h2 className="text-base font-semibold text-gray-900 mb-1">Teslimat adresi</h2>
582
+ <p className="text-sm text-gray-500 mb-4">Adresinizi aratarak hızlıca doldurabilir veya aşağıdaki alanları manuel girebilirsiniz.</p>
583
+
584
+ <div className="mb-4">
585
+ <AddressSearchInput
586
+ query={addressSearchQuery}
587
+ onQueryChange={handleAddressSearchQueryChange}
588
+ results={addressSearchResults}
589
+ isSearching={isAddressSearching}
590
+ showResults={showAddressSearchResults}
591
+ onShowResultsChange={setShowAddressSearchResults}
592
+ onSelect={placeId => void handleSelectGoogleAddress(placeId)}
593
+ error={errors.addressSearch?.message}
594
+ label="Adres ara"
595
+ placeholder="Mahalle, cadde, sokak veya posta kodu yazın…"
596
+ noResultsText="Sonuç bulunamadı"
597
+ />
598
+ </div>
599
+
600
+ <div className="space-y-4">
601
+ <div className="grid grid-cols-2 gap-4">
602
+ <Input {...register('firstName', { required: true })} label="Ad" placeholder="Ad" size="lg" required error={errors.firstName?.message} />
603
+ <Input {...register('lastName', { required: true })} label="Soyad" placeholder="Soyad" size="lg" required error={errors.lastName?.message} />
604
+ </div>
605
+ <Input {...register('street', { required: true })} label="Mahalle, cadde, sokak vb." placeholder="Mahalle, cadde, sokak vb." size="lg" required error={errors.street?.message} />
606
+ <div className="grid grid-cols-2 gap-4">
607
+ <Input {...register('buildingNumber')} label="Apartman no" placeholder="Apartman / bina no" size="lg" />
608
+ <Input {...register('apartmentNumber')} label="Daire no" placeholder="Daire no" size="lg" />
609
+ </div>
610
+ <div className="grid grid-cols-2 gap-4">
611
+ <Input {...register('postalCode', { required: true })} label="Posta Kodu" placeholder="Posta Kodu" size="lg" required error={errors.postalCode?.message} />
612
+ <Input
613
+ {...provinceField}
614
+ onChange={e => {
615
+ provinceField.onChange(e);
616
+ setValue('savedDeliveryAddressId', '');
617
+ }}
618
+ label="İl"
619
+ placeholder="İl"
620
+ size="lg"
621
+ required
622
+ error={errors.province?.message}
623
+ />
624
+ </div>
625
+ <div className="grid grid-cols-2 gap-4">
626
+ <Input
627
+ {...districtField}
628
+ onChange={e => {
629
+ districtField.onChange(e);
630
+ setValue('savedDeliveryAddressId', '');
631
+ }}
632
+ label="İlçe"
633
+ placeholder="İlçe"
634
+ size="lg"
635
+ required
636
+ error={errors.district?.message}
637
+ />
638
+ </div>
639
+ <div>
640
+ <label className="rizzui-input-label block text-sm mb-1.5 font-medium">Telefon *</label>
641
+ <div className="relative">
642
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
643
+ <span className="text-sm text-gray-700">🇹🇷</span>
644
+ <span className="text-sm text-gray-700">+90</span>
645
+ </div>
646
+ <Input {...register('phone', { required: true })} type="tel" placeholder="Telefon" size="lg" className="pl-20" required error={errors.phone?.message} />
647
+ </div>
648
+ </div>
649
+ </div>
650
+ </div>
651
+
652
+ <div>
653
+ <h2 className="text-base font-semibold text-gray-900 mb-2">Fatura adresi</h2>
654
+ <div className="flex items-start gap-2 mb-4">
655
+ <input
656
+ type="checkbox"
657
+ id="billingAddressDifferent"
658
+ {...register('billingAddressDifferent')}
659
+ className="mt-1 h-4 w-4 rounded border-gray-300 text-[#CF0A2C] focus:ring-[#CF0A2C]"
660
+ />
661
+ <label htmlFor="billingAddressDifferent" className="cursor-pointer text-sm text-gray-700">
662
+ Fatura adresi farklı olsun
663
+ </label>
664
+ </div>
665
+ {billingAddressDifferent && (
666
+ <div className="mt-4 space-y-4 rounded-xl border border-gray-200 bg-white p-4">
667
+ {savedAddresses.length > 0 && (
668
+ <div>
669
+ <label className="rizzui-input-label block text-sm mb-1.5 font-medium">Önerilen fatura adresi</label>
670
+ <select
671
+ value={savedBillingAddressId || (cart?.invoiceAddress?.id !== cart?.deliveryAddress?.id ? (cart?.invoiceAddress?.id ?? '') : '')}
672
+ onChange={e => {
673
+ if (!e.target.value) {
674
+ setValue('savedBillingAddressId', '');
675
+ return;
676
+ }
677
+ const addr = savedAddresses.find(a => a.id === e.target.value);
678
+ if (addr) handleSelectBillingAddress(addr);
679
+ }}
680
+ className="rizzui-input-container peer flex h-10 w-full items-center rounded-lg border border-gray-200 bg-white px-3.5 py-2 text-sm transition-all focus:border-[#CF0A2C] focus:outline-none focus:ring-2 focus:ring-[#CF0A2C]/25"
681
+ >
682
+ <option value="">Manuel girin</option>
683
+ {savedAddresses.map(addr => (
684
+ <option key={addr.id} value={addr.id}>
685
+ {addr.companyName || `${addr.firstName ?? ''} ${addr.lastName ?? ''}`.trim()}
686
+ {addr.administrativeAreaLevel2 ? ` — ${addr.administrativeAreaLevel2}, ${addr.administrativeAreaLevel1}` : ''}
687
+ </option>
688
+ ))}
689
+ </select>
690
+ </div>
691
+ )}
692
+ <p className="text-sm text-gray-600">Fatura bilgileri (şahıs adı veya firma unvanı, vergi numarası ve adres)</p>
693
+ <Input
694
+ {...register('billingFullName', {
695
+ required: billingAddressDifferent ? 'Ad / Firma adı zorunludur' : false,
696
+ })}
697
+ label="Ad / Firma adı"
698
+ placeholder="Şahıs için ad soyad, şirket için firma unvanı"
699
+ size="lg"
700
+ required={billingAddressDifferent}
701
+ error={errors.billingFullName?.message}
702
+ />
703
+ <Input
704
+ {...register('billingTaxNumber', {
705
+ required: billingAddressDifferent ? 'Vergi numarası zorunludur' : false,
706
+ })}
707
+ label="Vergi numarası"
708
+ placeholder="10 veya 11 haneli vergi numarası"
709
+ size="lg"
710
+ required={billingAddressDifferent}
711
+ error={errors.billingTaxNumber?.message}
712
+ />
713
+ <div className="grid grid-cols-2 gap-4">
714
+ <Input
715
+ {...billingProvinceField}
716
+ onChange={e => {
717
+ billingProvinceField.onChange(e);
718
+ setValue('savedBillingAddressId', '');
719
+ }}
720
+ label="İl"
721
+ placeholder="İl"
722
+ size="lg"
723
+ required={billingAddressDifferent}
724
+ error={errors.billingProvince?.message}
725
+ />
726
+ <Input
727
+ {...billingDistrictField}
728
+ onChange={e => {
729
+ billingDistrictField.onChange(e);
730
+ setValue('savedBillingAddressId', '');
731
+ }}
732
+ label="İlçe"
733
+ placeholder="İlçe"
734
+ size="lg"
735
+ required={billingAddressDifferent}
736
+ error={errors.billingDistrict?.message}
737
+ />
738
+ </div>
739
+ <Input
740
+ {...register('billingStreet', {
741
+ required: billingAddressDifferent ? 'Açık adres zorunludur' : false,
742
+ })}
743
+ label="Açık adres"
744
+ placeholder="Mahalle, cadde, sokak, bina no, daire no"
745
+ size="lg"
746
+ required={billingAddressDifferent}
747
+ error={errors.billingStreet?.message}
748
+ />
749
+ </div>
750
+ )}
751
+ </div>
752
+
753
+ <div className="flex items-start gap-2">
754
+ <input type="checkbox" {...register('newsletter')} id="newsletter" className="mt-1 h-4 w-4 rounded border-gray-300 text-[#CF0A2C] focus:ring-[#CF0A2C]" />
755
+ <label htmlFor="newsletter" className="text-sm text-gray-700 flex items-center gap-1">
756
+ Beni haberlerden ve özel tekliflerden haberdar et
757
+ <HelpCircle className="w-4 h-4 text-gray-400" />
758
+ </label>
759
+ </div>
760
+
761
+ {errors.root?.message ? <p className="text-sm text-red-600">{errors.root.message}</p> : null}
762
+
763
+ <div className="flex flex-col gap-3 sm:flex-row">
764
+ <Button type="button" size="lg" variant="outline" className="rounded-xl border-2 border-gray-200 font-semibold text-gray-900 hover:border-gray-900" onClick={onBack}>
765
+ Geri
766
+ </Button>
767
+ <Button
768
+ type="submit"
769
+ size="lg"
770
+ className="flex-1 rounded-xl bg-[#CF0A2C] font-semibold text-white hover:bg-[#a80824]"
771
+ disabled={isSubmitting}
772
+ >
773
+ {isSubmitting ? 'Kaydediliyor…' : 'Devam et'}
774
+ </Button>
775
+ </div>
776
+ </form>
777
+ );
778
+ }
779
+
780
+ function ViewStepCheckout({ onBack }: { onBack: () => void }) {
781
+ const router = useRouter();
782
+ const { webReturnUrl } = useCheckoutViewConfig();
783
+ const { cart, cartLoading, paymentMethod, deliveryMethod, bankAccount } = useRaxon();
784
+ const [payStartError, setPayStartError] = useState<string | null>(null);
785
+ const [bankTransferError, setBankTransferError] = useState<string | null>(null);
786
+ const [selectedBankAccountId, setSelectedBankAccountId] = useState<string | null>(null);
787
+ const paymentDefaultRef = useRef(false);
788
+ const deliveryDefaultRef = useRef(false);
789
+ const addressBasketSyncRef = useRef(false);
790
+
791
+ const updateBasketMutation = useCart().update();
792
+ const { data: addressesPayload, isLoading: addressesLoading } = useAddress().fetch();
793
+ const { mutateAsync: payMutation, isPending: payMutationPending } = useCart().pay();
794
+ const { mutateAsync: createTransferCode, isPending: transferCodePending } = useCart().createTransferCode();
795
+
796
+ const selectedPaymentMethod = paymentMethod.find(m => m.id === cart?.paymentMethod?.id);
797
+ const isBankTransfer =
798
+ selectedPaymentMethod?.provider === PaymentProvider.BANK_TRANSFER &&
799
+ selectedPaymentMethod?.paymentTerms === PaymentTerms.BANK_TRANSFER;
800
+ const payAmount = cart?.info?.payPrice?.pay ?? 0;
801
+ const bankTransferCode = cart?.bankTransferCode;
802
+ const hasValidTransferCode =
803
+ Boolean(bankTransferCode?.id) &&
804
+ bankTransferCode != null &&
805
+ Math.abs((bankTransferCode.amount ?? 0) - payAmount) < 0.01;
806
+
807
+ useEffect(() => {
808
+ void resolveClientIp();
809
+ }, []);
810
+
811
+ useEffect(() => {
812
+ setPayStartError(null);
813
+ setBankTransferError(null);
814
+ if (!isBankTransfer) {
815
+ setSelectedBankAccountId(null);
816
+ }
817
+ }, [cart?.paymentMethod?.id, isBankTransfer]);
818
+
819
+ useEffect(() => {
820
+ if (!isBankTransfer) return;
821
+ if (bankTransferCode?.bankAccount?.id) {
822
+ setSelectedBankAccountId(bankTransferCode.bankAccount.id);
823
+ }
824
+ }, [isBankTransfer, bankTransferCode?.bankAccount?.id]);
825
+
826
+ const handleBankAccountSelect = async (account: BankAccount) => {
827
+ if (!isBankTransfer || !cart?.id || transferCodePending || updateBasketMutation.isPending) return;
828
+
829
+ setSelectedBankAccountId(account.id);
830
+ setBankTransferError(null);
831
+
832
+ if (
833
+ hasValidTransferCode &&
834
+ bankTransferCode?.bankAccount?.id === account.id
835
+ ) {
836
+ return;
837
+ }
838
+
839
+ try {
840
+ const code = await createTransferCode({
841
+ amount: payAmount,
842
+ bankAccountId: account.id,
843
+ });
844
+ await updateBasketMutation.mutateAsync({
845
+ id: cart.id,
846
+ bankTransferCodeId: code.id,
847
+ });
848
+ } catch {
849
+ setBankTransferError('Transfer kodu oluşturulamadı. Lütfen tekrar deneyin.');
850
+ }
851
+ };
852
+
853
+ useEffect(() => {
854
+ paymentDefaultRef.current = false;
855
+ deliveryDefaultRef.current = false;
856
+ addressBasketSyncRef.current = false;
857
+ }, [cart?.id]);
858
+
859
+ const savedAddresses: Address[] = addressesPayload?.data ?? [];
860
+ const defaultSavedAddress = savedAddresses.length ? pickDefaultDeliveryAddress(savedAddresses) : undefined;
861
+
862
+ useEffect(() => {
863
+ if (updateBasketMutation.isPending) return;
864
+ if (!cart?.id || cart?.deliveryAddress?.id) return;
865
+ if (!defaultSavedAddress?.id) return;
866
+ if (addressBasketSyncRef.current) return;
867
+ addressBasketSyncRef.current = true;
868
+
869
+ updateBasketMutation.mutate({
870
+ id: cart.id,
871
+ deliveryAddressId: defaultSavedAddress.id,
872
+ invoiceAddressId: defaultSavedAddress.id,
873
+ });
874
+ }, [cart?.id, cart?.deliveryAddress?.id, defaultSavedAddress?.id, updateBasketMutation.isPending, updateBasketMutation.mutate]);
875
+
876
+ useEffect(() => {
877
+ if (updateBasketMutation.isPending) return;
878
+ if (!cart?.id) return;
879
+ const list = paymentMethod;
880
+ if (!list?.length || paymentDefaultRef.current) return;
881
+ if (cart?.paymentMethod?.id) {
882
+ paymentDefaultRef.current = true;
883
+ return;
884
+ }
885
+ paymentDefaultRef.current = true;
886
+ updateBasketMutation.mutate({ id: cart.id, paymentMethodId: list[0].id });
887
+ }, [paymentMethod, cart?.id, cart?.paymentMethod?.id, updateBasketMutation.isPending, updateBasketMutation.mutate]);
888
+
889
+ useEffect(() => {
890
+ if (updateBasketMutation.isPending) return;
891
+ if (!cart?.id) return;
892
+ const list = deliveryMethod;
893
+ if (!list?.length || deliveryDefaultRef.current) return;
894
+ if (cart?.deliveryMethod?.id) {
895
+ deliveryDefaultRef.current = true;
896
+ return;
897
+ }
898
+ deliveryDefaultRef.current = true;
899
+ updateBasketMutation.mutate({ id: cart.id, deliveryMethodId: list[0].id });
900
+ }, [deliveryMethod, cart?.id, cart?.deliveryMethod?.id, updateBasketMutation.isPending, updateBasketMutation.mutate]);
901
+
902
+ const delivery = cart?.deliveryAddress ?? defaultSavedAddress ?? undefined;
903
+ const invoice = cart?.invoiceAddress;
904
+ const invoiceSameAsDelivery = delivery?.id && invoice?.id === delivery.id;
905
+ const deliveryLine = delivery ? formatAddressLine(delivery) : null;
906
+ const invoiceLine = invoice && !invoiceSameAsDelivery ? formatAddressLine(invoice) : null;
907
+ const isAddressLoading = !deliveryLine && (cartLoading || addressesLoading || updateBasketMutation.isPending);
908
+
909
+ const paymentBlockers = useMemo(() => {
910
+ const blockers: string[] = [];
911
+
912
+ if (cartLoading) {
913
+ blockers.push('Sepet yükleniyor…');
914
+ return blockers;
915
+ }
916
+
917
+ if (!cart?.email?.trim()) {
918
+ blockers.push('E-posta adresi girilmedi.');
919
+ }
920
+
921
+ if (!cart?.deliveryAddress) {
922
+ if (isAddressLoading) {
923
+ blockers.push('Teslimat adresi hazırlanıyor…');
924
+ } else {
925
+ blockers.push('Teslimat adresi eklenmedi.');
926
+ }
927
+ }
928
+
929
+ if (paymentMethod.length === 0) {
930
+ blockers.push('Ödeme yöntemi bulunamadı.');
931
+ } else if (!cart?.paymentMethod?.id) {
932
+ blockers.push('Ödeme yöntemi seçin.');
933
+ }
934
+
935
+ if (deliveryMethod.length === 0) {
936
+ blockers.push('Teslimat yöntemi bulunamadı.');
937
+ } else if (!cart?.deliveryMethod?.id) {
938
+ blockers.push('Teslimat yöntemi seçin.');
939
+ }
940
+
941
+ if (isBankTransfer) {
942
+ if (bankAccount.length === 0) {
943
+ blockers.push('Havale için tanımlı banka hesabı bulunamadı.');
944
+ } else if (!hasValidTransferCode) {
945
+ if (!selectedBankAccountId && !bankTransferCode?.bankAccount?.id) {
946
+ blockers.push('Devam etmek için bir banka hesabı seçin.');
947
+ } else if (transferCodePending || updateBasketMutation.isPending) {
948
+ blockers.push('Transfer kodu hazırlanıyor…');
949
+ } else {
950
+ blockers.push('Transfer kodu oluşturulamadı. Banka hesabını tekrar seçin.');
951
+ }
952
+ }
953
+ }
954
+
955
+ return blockers;
956
+ }, [
957
+ cart?.email,
958
+ cart?.deliveryAddress,
959
+ cart?.paymentMethod?.id,
960
+ cart?.deliveryMethod?.id,
961
+ cartLoading,
962
+ isAddressLoading,
963
+ paymentMethod.length,
964
+ deliveryMethod.length,
965
+ isBankTransfer,
966
+ bankAccount.length,
967
+ hasValidTransferCode,
968
+ selectedBankAccountId,
969
+ bankTransferCode?.bankAccount?.id,
970
+ transferCodePending,
971
+ updateBasketMutation.isPending,
972
+ ]);
973
+
974
+ const canCompletePayment = paymentBlockers.length === 0;
975
+
976
+ const isPaymentButtonDisabled =
977
+ !canCompletePayment || payMutationPending || transferCodePending;
978
+
979
+ const handlePaymentSubmit = async () => {
980
+ if (!canCompletePayment) return;
981
+ setPayStartError(null);
982
+ try {
983
+ const payReturnUrl = resolveAbsoluteReturnUrl(webReturnUrl);
984
+ const paymentResponse = await payMutation({ platform: 'web', webReturnUrl: payReturnUrl });
985
+ const ok = paymentResponse && typeof paymentResponse === 'object' && 'status' in paymentResponse && (paymentResponse as { status: unknown }).status === true;
986
+ const info =
987
+ paymentResponse && typeof paymentResponse === 'object' && 'info' in paymentResponse && paymentResponse.info && typeof paymentResponse.info === 'object'
988
+ ? (paymentResponse.info as Record<string, unknown>)
989
+ : null;
990
+
991
+ if (isBankTransfer) {
992
+ const orderId = info && typeof info.orderId === 'string' ? info.orderId : null;
993
+ if (ok && orderId) {
994
+ router.push(`/hesabim/siparislerim/${orderId}`);
995
+ return;
996
+ }
997
+ setPayStartError('Sipariş oluşturulamadı. Lütfen tekrar deneyin.');
998
+ return;
999
+ }
1000
+
1001
+ const provider = info && typeof info.provider === 'string' ? info.provider : null;
1002
+ const garantiHtml = info && typeof info.html === 'string' ? info.html : null;
1003
+ const garantiTransactionId = info && typeof info.transactionId === 'string' ? info.transactionId : null;
1004
+
1005
+ if (ok && provider === PaymentProvider.GARANTI && garantiHtml && garantiTransactionId) {
1006
+ storeGarantiPaymentHtml(garantiTransactionId, garantiHtml);
1007
+ router.push(
1008
+ withCheckoutQuery(webReturnUrl, `type=pay&provider=garanti&transactionId=${encodeURIComponent(garantiTransactionId)}`),
1009
+ );
1010
+ return;
1011
+ }
1012
+
1013
+ const token = info && typeof info.token === 'string' ? info.token : null;
1014
+ if (ok && token) {
1015
+ router.push(withCheckoutQuery(webReturnUrl, `type=pay&token=${encodeURIComponent(token)}`));
1016
+ } else {
1017
+ setPayStartError('Ödeme ekranı açılamadı. Lütfen tekrar deneyin.');
1018
+ }
1019
+ } catch {
1020
+ setPayStartError('Ödeme başlatılırken bir hata oluştu.');
1021
+ }
1022
+ };
1023
+
1024
+ return (
1025
+ <div className="space-y-6">
1026
+ <div>
1027
+ <h2 className="text-base font-semibold text-gray-900">Sipariş özeti</h2>
1028
+ <p className="mt-1 text-sm text-gray-500">Adres bilgilerinizi kontrol edin ve ödeme yöntemini seçin.</p>
1029
+ </div>
1030
+
1031
+ {isAddressLoading ? (
1032
+ <p className="text-sm text-gray-500">Sepet güncelleniyor…</p>
1033
+ ) : !deliveryLine ? (
1034
+ <p className="text-sm text-amber-700 rounded-xl border border-amber-200 bg-amber-50 p-4">Teslimat adresi henüz eklenmedi.</p>
1035
+ ) : (
1036
+ <div className="rounded-xl border border-gray-200 bg-gray-50 p-4 space-y-1">
1037
+ <p className="text-xs font-black uppercase tracking-widest text-[#CF0A2C]">Teslimat adresi</p>
1038
+ <p className="text-sm font-medium text-gray-900">{deliveryLine.name}</p>
1039
+ <p className="text-sm text-gray-600">{deliveryLine.street}</p>
1040
+ <p className="text-sm text-gray-600">
1041
+ {[deliveryLine.location, deliveryLine.postalCode].filter(Boolean).join(' · ')}
1042
+ </p>
1043
+ </div>
1044
+ )}
1045
+
1046
+ {invoiceLine ? (
1047
+ <div className="rounded-xl border border-gray-200 bg-gray-50 p-4 space-y-1">
1048
+ <p className="text-xs font-black uppercase tracking-widest text-[#CF0A2C]">Fatura adresi</p>
1049
+ <p className="text-sm font-medium text-gray-900">{invoiceLine.name}</p>
1050
+ {invoiceLine.taxNumber ? <p className="text-sm text-gray-600">VKN: {invoiceLine.taxNumber}</p> : null}
1051
+ <p className="text-sm text-gray-600">{invoiceLine.street}</p>
1052
+ <p className="text-sm text-gray-600">
1053
+ {[invoiceLine.location, invoiceLine.postalCode].filter(Boolean).join(' · ')}
1054
+ </p>
1055
+ </div>
1056
+ ) : deliveryLine ? (
1057
+ <p className="text-sm text-gray-500">Fatura adresi teslimat adresi ile aynı.</p>
1058
+ ) : null}
1059
+
1060
+ <div className="rounded-xl border border-gray-100 bg-white p-4 space-y-2">
1061
+ <div className="flex justify-between text-sm">
1062
+ <span className="text-gray-600">Ara toplam</span>
1063
+ <span className="font-semibold tabular-nums">{(cart?.info?.basePrice?.total ?? 0).toTry()}</span>
1064
+ </div>
1065
+ {(cart?.info?.delivery?.pay ?? 0) > 0 ? (
1066
+ <div className="flex justify-between text-sm">
1067
+ <span className="text-gray-600">Kargo</span>
1068
+ <span className="font-semibold tabular-nums">{cart?.info?.delivery?.pay?.toTry()}</span>
1069
+ </div>
1070
+ ) : null}
1071
+ <div className="flex justify-between border-t border-gray-100 pt-2 text-sm">
1072
+ <span className="font-semibold text-gray-900">Toplam</span>
1073
+ <span className="font-bold tabular-nums text-[#CF0A2C]">{(cart?.info?.payPrice?.pay ?? 0).toTry()}</span>
1074
+ </div>
1075
+ </div>
1076
+
1077
+ <div className="border-t border-gray-100 pt-6">
1078
+ <h2 className="text-base font-semibold text-gray-900 mb-3">Teslimat yöntemi</h2>
1079
+ <div className="space-y-2">
1080
+ {deliveryMethod.map((method: DeliveryMethod) => (
1081
+ <label key={method.id} className="flex cursor-pointer items-start gap-3 rounded-xl border border-gray-100 bg-white p-4 shadow-sm transition-colors hover:border-gray-200 has-[:checked]:border-[#CF0A2C] has-[:checked]:ring-2 has-[:checked]:ring-[#CF0A2C]/20">
1082
+ <input
1083
+ type="radio"
1084
+ name="deliveryMethod"
1085
+ value={method.id}
1086
+ checked={cart?.deliveryMethod?.id === method.id}
1087
+ onChange={() => cart?.id && updateBasketMutation.mutate({ id: cart.id, deliveryMethodId: method.id })}
1088
+ disabled={updateBasketMutation.isPending}
1089
+ className="mt-0.5 h-4 w-4 shrink-0 border-gray-300 text-[#CF0A2C] focus:ring-[#CF0A2C]"
1090
+ />
1091
+ <span className="flex flex-col gap-0.5 min-w-0">
1092
+ <span className="text-sm font-medium text-gray-900">{method.name}</span>
1093
+ </span>
1094
+ </label>
1095
+ ))}
1096
+ </div>
1097
+ </div>
1098
+
1099
+ <div>
1100
+ <h2 className="text-base font-semibold text-gray-900 mb-3">Ödeme yöntemi</h2>
1101
+ <div className="space-y-2">
1102
+ {paymentMethod.map((method: PaymentMethod) => (
1103
+ <label key={method.id} className="flex cursor-pointer items-start gap-3 rounded-xl border border-gray-100 bg-white p-4 shadow-sm transition-colors hover:border-gray-200 has-[:checked]:border-[#CF0A2C] has-[:checked]:ring-2 has-[:checked]:ring-[#CF0A2C]/20">
1104
+ <input
1105
+ type="radio"
1106
+ name="payment"
1107
+ value={method.id}
1108
+ checked={cart?.paymentMethod?.id === method.id}
1109
+ onChange={() => cart?.id && updateBasketMutation.mutate({ id: cart.id, paymentMethodId: method.id })}
1110
+ disabled={updateBasketMutation.isPending}
1111
+ className="mt-0.5 h-4 w-4 shrink-0 border-gray-300 text-[#CF0A2C] focus:ring-[#CF0A2C]"
1112
+ />
1113
+ <span className="flex flex-col gap-0.5 min-w-0">
1114
+ <span className="text-sm font-medium text-gray-900">{method.name}</span>
1115
+ {method.description ? <span className="text-xs text-gray-600">{method.description}</span> : null}
1116
+ </span>
1117
+ </label>
1118
+ ))}
1119
+ </div>
1120
+ </div>
1121
+
1122
+ {isBankTransfer ? (
1123
+ <div className="space-y-4">
1124
+ <div>
1125
+ <h2 className="text-base font-semibold text-gray-900 mb-3">Banka hesabı</h2>
1126
+ {bankAccount.length === 0 ? (
1127
+ <p className="text-sm text-amber-700 rounded-xl border border-amber-200 bg-amber-50 p-4">
1128
+ Havale için tanımlı banka hesabı bulunamadı.
1129
+ </p>
1130
+ ) : (
1131
+ <div className="space-y-2">
1132
+ {bankAccount.map((account: BankAccount) => (
1133
+ <label
1134
+ key={account.id}
1135
+ className="flex cursor-pointer items-start gap-3 rounded-xl border border-gray-100 bg-white p-4 shadow-sm transition-colors hover:border-gray-200 has-[:checked]:border-[#CF0A2C] has-[:checked]:ring-2 has-[:checked]:ring-[#CF0A2C]/20"
1136
+ >
1137
+ <input
1138
+ type="radio"
1139
+ name="bankAccount"
1140
+ value={account.id}
1141
+ checked={selectedBankAccountId === account.id}
1142
+ onChange={() => void handleBankAccountSelect(account)}
1143
+ disabled={transferCodePending || updateBasketMutation.isPending}
1144
+ className="mt-0.5 h-4 w-4 shrink-0 border-gray-300 text-[#CF0A2C] focus:ring-[#CF0A2C]"
1145
+ />
1146
+ <span className="flex flex-col gap-0.5 min-w-0">
1147
+ <span className="text-sm font-medium text-gray-900">{account.bankName}</span>
1148
+ {account.accountHolderName ? (
1149
+ <span className="text-xs text-gray-600">{account.accountHolderName}</span>
1150
+ ) : null}
1151
+ {account.IBAN && account.IBAN !== '-' ? (
1152
+ <span className="text-xs font-mono text-gray-500">{account.IBAN}</span>
1153
+ ) : null}
1154
+ </span>
1155
+ </label>
1156
+ ))}
1157
+ </div>
1158
+ )}
1159
+ </div>
1160
+
1161
+ {transferCodePending || updateBasketMutation.isPending ? (
1162
+ <p className="text-sm text-gray-500">Transfer kodu hazırlanıyor…</p>
1163
+ ) : null}
1164
+
1165
+ {bankTransferError ? (
1166
+ <p className="text-sm text-red-600" role="alert">
1167
+ {bankTransferError}
1168
+ </p>
1169
+ ) : null}
1170
+
1171
+ {hasValidTransferCode && bankTransferCode ? (
1172
+ <div className="rounded-xl border border-[#CF0A2C]/20 bg-[#CF0A2C]/5 p-4 space-y-3">
1173
+ <p className="text-xs font-black uppercase tracking-widest text-[#CF0A2C]">Havale bilgileri</p>
1174
+ <div className="space-y-2 text-sm">
1175
+ <div>
1176
+ <p className="text-gray-500">Banka</p>
1177
+ <p className="font-medium text-gray-900">{bankTransferCode.bankAccount.bankName}</p>
1178
+ </div>
1179
+ {bankTransferCode.bankAccount.IBAN && bankTransferCode.bankAccount.IBAN !== '-' ? (
1180
+ <div>
1181
+ <p className="text-gray-500">IBAN</p>
1182
+ <p className="font-mono font-medium text-gray-900">{bankTransferCode.bankAccount.IBAN}</p>
1183
+ </div>
1184
+ ) : null}
1185
+ {bankTransferCode.bankAccount.accountHolderName ? (
1186
+ <div>
1187
+ <p className="text-gray-500">Alıcı adı</p>
1188
+ <p className="font-medium text-gray-900">{bankTransferCode.bankAccount.accountHolderName}</p>
1189
+ </div>
1190
+ ) : null}
1191
+ <div>
1192
+ <p className="text-gray-500">Tutar</p>
1193
+ <p className="font-bold tabular-nums text-[#CF0A2C]">{(bankTransferCode.amount ?? 0).toTry()}</p>
1194
+ </div>
1195
+ <div className="rounded-lg bg-white p-3 border border-[#CF0A2C]/20">
1196
+ <p className="text-xs text-gray-500">Transfer kodu (açıklama alanına yazın)</p>
1197
+ <p className="text-lg font-black tracking-wider text-[#CF0A2C]">{bankTransferCode.code}</p>
1198
+ </div>
1199
+ </div>
1200
+ <p className="text-xs text-gray-600">
1201
+ Havaleyi yaptıktan sonra &quot;Siparişi tamamla&quot; butonuna basın. Ödemeniz onaylandığında siparişiniz işleme alınır.
1202
+ </p>
1203
+ </div>
1204
+ ) : isBankTransfer && bankAccount.length > 0 ? (
1205
+ <p className="text-sm text-gray-500">Devam etmek için bir banka hesabı seçin.</p>
1206
+ ) : null}
1207
+ </div>
1208
+ ) : null}
1209
+
1210
+ {payStartError && (
1211
+ <p className="text-sm text-red-600" role="alert">
1212
+ {payStartError}
1213
+ </p>
1214
+ )}
1215
+
1216
+ {paymentBlockers.length > 0 && !payMutationPending ? (
1217
+ <div className="rounded-xl border border-red-200 bg-red-50 p-4" role="alert">
1218
+ <p className="text-sm font-medium text-red-800">Ödeme tamamlanamıyor:</p>
1219
+ <ul className="mt-1.5 list-inside list-disc space-y-0.5 text-sm text-red-600">
1220
+ {paymentBlockers.map(message => (
1221
+ <li key={message}>{message}</li>
1222
+ ))}
1223
+ </ul>
1224
+ </div>
1225
+ ) : null}
1226
+
1227
+ <div className="flex flex-col gap-3 sm:flex-row">
1228
+ <Button type="button" size="lg" variant="outline" className="rounded-xl border-2 border-gray-200 font-semibold text-gray-900 hover:border-gray-900" onClick={onBack}>
1229
+ Geri
1230
+ </Button>
1231
+ <Button
1232
+ type="button"
1233
+ size="lg"
1234
+ className={`flex-1 rounded-xl font-black uppercase tracking-widest shadow-md ${
1235
+ isPaymentButtonDisabled
1236
+ ? 'cursor-not-allowed bg-gray-300 text-gray-500 hover:bg-gray-300 hover:shadow-md'
1237
+ : 'bg-[#CF0A2C] text-white hover:bg-[#a80824] hover:shadow-lg'
1238
+ }`}
1239
+ onClick={handlePaymentSubmit}
1240
+ disabled={isPaymentButtonDisabled}
1241
+ >
1242
+ {payMutationPending ? 'Yönlendiriliyor…' : isBankTransfer ? 'Siparişi tamamla' : 'Ödemeyi tamamla'}
1243
+ </Button>
1244
+ </div>
1245
+ </div>
1246
+ );
1247
+ }
1248
+
1249
+ const CHECKOUT_STEP_LABELS: Record<CheckoutStep, string> = {
1250
+ contact: 'İletişim',
1251
+ address: 'Adres',
1252
+ checkout: 'Önizleme ve ödeme',
1253
+ };
1254
+
1255
+ const PAYTR_IFRAME_ID = 'paytriframe';
1256
+
1257
+ const TERMINAL_FAILURE_STATUSES: Set<Status> = new Set([
1258
+ Status.REJECTED,
1259
+ Status.ERROR,
1260
+ Status.CANCELLED,
1261
+ Status.TIMEOUT,
1262
+ ]);
1263
+
1264
+ function normalizeReturnMessage(raw: string | null): string | null {
1265
+ if (raw == null || raw === '') return null;
1266
+ try {
1267
+ const d = decodeURIComponent(raw.replace(/\+/g, ' '));
1268
+ return d.replace(/^['"]|['"]$/g, '').trim() || null;
1269
+ } catch {
1270
+ return raw.replace(/^['"]|['"]$/g, '').trim() || null;
1271
+ }
1272
+ }
1273
+
1274
+ function shortenId(id: string): string {
1275
+ if (id.length <= 12) return id;
1276
+ return `${id.slice(0, 8)}…${id.slice(-4)}`;
1277
+ }
1278
+
1279
+ async function fetchTransactionStatus(transactionId: string): Promise<{ status: Status | null; orderId: string | null }> {
1280
+ const response = await nexineAxios.get<{ status?: Status; orderId?: string | null }>(
1281
+ `/global/transcation/${transactionId}/status`,
1282
+ );
1283
+ return {
1284
+ status: response.data?.status ?? null,
1285
+ orderId: response.data?.orderId ?? null,
1286
+ };
1287
+ }
1288
+
1289
+ function runPaytrIframeResize() {
1290
+ const w = window as Window & { iFrameResize?: (opts: Record<string, unknown>, selector: string) => void };
1291
+ w.iFrameResize?.({}, `#${PAYTR_IFRAME_ID}`);
1292
+ }
1293
+
1294
+ function CheckoutShellHeader() {
1295
+ const { branch } = useRaxon();
1296
+ return (
1297
+ <header className="border-b border-gray-100 bg-white">
1298
+ <div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-5 sm:px-6">
1299
+ <Link href="/" className="flex flex-col items-start gap-1">
1300
+ <GeneralImage
1301
+ quality={85}
1302
+ src={branch?.logoMedia?.relativePath ? `${process.env.NEXT_PUBLIC_STORAGE_URL}/${branch.logoMedia.relativePath}` : ''}
1303
+ alt={branch?.tradingName ?? 'Logo'}
1304
+ width={120}
1305
+ height={36}
1306
+ className="h-7 w-auto object-contain"
1307
+ />
1308
+ </Link>
1309
+ </div>
1310
+ </header>
1311
+ );
1312
+ }
1313
+
1314
+ function CheckoutPayView() {
1315
+ const searchParams = useSearchParams();
1316
+ const { webReturnUrl } = useCheckoutViewConfig();
1317
+
1318
+ const token = searchParams.get('token');
1319
+ const providerParam = searchParams.get('provider');
1320
+ const transactionId = searchParams.get('transactionId');
1321
+
1322
+ const [garantiRedirectError, setGarantiRedirectError] = useState<string | null>(null);
1323
+ const [garantiRedirecting, setGarantiRedirecting] = useState(false);
1324
+ const garantiSubmitRef = useRef(false);
1325
+
1326
+ const isGarantiRedirect =
1327
+ providerParam === 'garanti' &&
1328
+ transactionId != null &&
1329
+ transactionId.trim() !== '';
1330
+
1331
+ useEffect(() => {
1332
+ if (!isGarantiRedirect || !transactionId || garantiSubmitRef.current) return;
1333
+
1334
+ const html = consumeGarantiPaymentHtml(transactionId);
1335
+ if (!html) {
1336
+ setGarantiRedirectError('Garanti ödeme oturumu bulunamadı. Lütfen ödeme adımından tekrar deneyin.');
1337
+ return;
1338
+ }
1339
+
1340
+ garantiSubmitRef.current = true;
1341
+ setGarantiRedirecting(true);
1342
+
1343
+ try {
1344
+ submitGarantiPaymentHtml(html);
1345
+ } catch {
1346
+ garantiSubmitRef.current = false;
1347
+ setGarantiRedirecting(false);
1348
+ setGarantiRedirectError('Garanti ödeme sayfasına yönlendirilemedi. Lütfen tekrar deneyin.');
1349
+ }
1350
+ }, [isGarantiRedirect, transactionId]);
1351
+
1352
+ useEffect(() => {
1353
+ if (!token) return;
1354
+ const scriptSelector = 'script[data-paytr-iframe-resizer]';
1355
+ if (document.querySelector(scriptSelector)) {
1356
+ runPaytrIframeResize();
1357
+ return;
1358
+ }
1359
+ const s = document.createElement('script');
1360
+ s.src = 'https://www.paytr.com/js/iframeResizer.min.js';
1361
+ s.async = true;
1362
+ s.dataset.paytrIframeResizer = 'true';
1363
+ s.onload = runPaytrIframeResize;
1364
+ document.body.appendChild(s);
1365
+ }, [token]);
1366
+
1367
+ if (isGarantiRedirect) {
1368
+ if (garantiRedirectError) {
1369
+ return (
1370
+ <div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center px-4">
1371
+ <div className="max-w-md w-full rounded-2xl border border-gray-100 bg-white p-10 text-center shadow-sm">
1372
+ <h1 className="text-xl font-black uppercase tracking-tighter text-gray-900 mb-3">Garanti ödeme</h1>
1373
+ <p className="text-sm text-[#CF0A2C] mb-8 font-medium">{garantiRedirectError}</p>
1374
+ <Link href={webReturnUrl} className="inline-flex justify-center rounded-xl bg-[#CF0A2C] px-8 py-3.5 text-sm font-black uppercase tracking-widest text-white transition hover:bg-[#a80824]">
1375
+ Ödemeye dön
1376
+ </Link>
1377
+ </div>
1378
+ </div>
1379
+ );
1380
+ }
1381
+
1382
+ return (
1383
+ <div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center px-4">
1384
+ <div className="max-w-md w-full rounded-2xl border border-gray-100 bg-white p-10 text-center shadow-sm">
1385
+ <div className="mx-auto mb-5 h-10 w-10 animate-spin rounded-full border-2 border-gray-200 border-t-[#CF0A2C]" aria-hidden />
1386
+ <span className="text-sm font-black text-[#CF0A2C] uppercase tracking-widest">Garanti</span>
1387
+ <h1 className="mt-2 text-xl font-black uppercase tracking-tighter text-gray-900 mb-3">
1388
+ {garantiRedirecting ? 'Yönlendiriliyorsunuz' : 'Hazırlanıyor'}
1389
+ </h1>
1390
+ <p className="text-sm text-gray-600 leading-relaxed">
1391
+ Garanti güvenli ödeme sayfasına aktarılıyorsunuz. Lütfen bekleyin.
1392
+ </p>
1393
+ </div>
1394
+ </div>
1395
+ );
1396
+ }
1397
+
1398
+ if (token) {
1399
+ return (
1400
+ <div className="min-h-screen bg-gray-50">
1401
+ <div className="max-w-3xl mx-auto px-4 py-10 sm:py-12">
1402
+ <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
1403
+ <div>
1404
+ <span className="text-sm font-black text-[#CF0A2C] uppercase tracking-widest">PayTR</span>
1405
+ <h1 className="mt-2 text-2xl font-black text-gray-900 uppercase tracking-tighter">Güvenli ödeme</h1>
1406
+ </div>
1407
+ <Link href={webReturnUrl} className="text-xs font-black uppercase tracking-widest text-gray-900 border-b-2 border-black pb-1 self-start sm:self-auto hover:text-[#CF0A2C] hover:border-[#CF0A2C] transition-colors">
1408
+ Ödeme sayfasına dön
1409
+ </Link>
1410
+ </div>
1411
+ <div className="rounded-2xl border border-gray-100 bg-white overflow-hidden shadow-sm">
1412
+ <div className="p-2">
1413
+ <iframe
1414
+ title="PayTR güvenli ödeme"
1415
+ src={`https://www.paytr.com/odeme/guvenli/${token}`}
1416
+ id={PAYTR_IFRAME_ID}
1417
+ frameBorder={0}
1418
+ scrolling="no"
1419
+ className="w-full min-h-[480px] border-0 rounded-xl"
1420
+ onLoad={runPaytrIframeResize}
1421
+ />
1422
+ </div>
1423
+ </div>
1424
+ </div>
1425
+ </div>
1426
+ );
1427
+ }
1428
+
1429
+ return (
1430
+ <div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center px-4">
1431
+ <div className="max-w-md w-full rounded-2xl border border-gray-100 bg-white p-10 text-center shadow-sm">
1432
+ <h1 className="text-xl font-black uppercase tracking-tighter text-gray-900 mb-3">Geçersiz bağlantı</h1>
1433
+ <p className="text-sm text-gray-600 mb-8 leading-relaxed">Ödeme oturumu bulunamadı. Lütfen ödeme adımından tekrar deneyin.</p>
1434
+ <Link href={webReturnUrl} className="inline-flex justify-center rounded-xl bg-[#CF0A2C] px-8 py-3.5 text-sm font-black uppercase tracking-widest text-white transition hover:bg-[#a80824]">
1435
+ Ödemeye git
1436
+ </Link>
1437
+ </div>
1438
+ </div>
1439
+ );
1440
+ }
1441
+
1442
+ type ResultPhase = 'loading' | 'polling' | 'success' | 'error';
1443
+
1444
+ function CheckoutResultView() {
1445
+ const searchParams = useSearchParams();
1446
+ const queryClient = useQueryClient();
1447
+ const { webReturnUrl } = useCheckoutViewConfig();
1448
+
1449
+ const statusParam = searchParams.get('status');
1450
+ const titleParam = normalizeReturnMessage(searchParams.get('title'));
1451
+ const messageParam = normalizeReturnMessage(searchParams.get('message'));
1452
+ const transactionId = searchParams.get('transactionId');
1453
+ const paymentIdParam = searchParams.get('paymentId');
1454
+ const orderIdParam = searchParams.get('orderId');
1455
+
1456
+ const isSuccess = statusParam === 'success';
1457
+ const isFailure = statusParam === 'fail' || statusParam === 'error';
1458
+ const hasValidStatus = isSuccess || isFailure;
1459
+
1460
+ const [phase, setPhase] = useState<ResultPhase>(() => {
1461
+ if (isFailure) return 'error';
1462
+ if (isSuccess && orderIdParam) return 'success';
1463
+ if (isSuccess) return 'loading';
1464
+ return 'loading';
1465
+ });
1466
+ const [resolvedOrderId, setResolvedOrderId] = useState<string | null>(orderIdParam);
1467
+ const resolvedPaymentId = paymentIdParam;
1468
+ const [errorMessage, setErrorMessage] = useState<string | null>(
1469
+ isFailure ? messageParam ?? titleParam ?? 'Ödeme tamamlanamadı.' : null,
1470
+ );
1471
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
1472
+ const pollFailCountRef = useRef(0);
1473
+ const cartInvalidatedRef = useRef(false);
1474
+
1475
+ const clearPollInterval = useCallback(() => {
1476
+ if (intervalRef.current != null) {
1477
+ clearInterval(intervalRef.current);
1478
+ intervalRef.current = null;
1479
+ }
1480
+ }, []);
1481
+
1482
+ const { detail } = useOrder();
1483
+ const orderQuery = detail(resolvedOrderId ?? '');
1484
+ const order = orderQuery.data;
1485
+
1486
+ useEffect(() => {
1487
+ if (window.self !== window.top) {
1488
+ window.top!.location.href = window.location.href;
1489
+ }
1490
+ }, []);
1491
+
1492
+ useEffect(() => {
1493
+ if (!isSuccess) return;
1494
+
1495
+ if (orderIdParam) {
1496
+ setResolvedOrderId(orderIdParam);
1497
+ setPhase('success');
1498
+ return;
1499
+ }
1500
+
1501
+ if (!transactionId) {
1502
+ setErrorMessage('İşlem bilgisi bulunamadı.');
1503
+ setPhase('error');
1504
+ return;
1505
+ }
1506
+
1507
+ let cancelled = false;
1508
+ setPhase('polling');
1509
+
1510
+ const tick = async () => {
1511
+ try {
1512
+ const { status: st, orderId } = await fetchTransactionStatus(transactionId);
1513
+ pollFailCountRef.current = 0;
1514
+ if (cancelled) return;
1515
+
1516
+ if (orderId) {
1517
+ setResolvedOrderId(orderId);
1518
+ }
1519
+
1520
+ if (st === Status.COMPLETED) {
1521
+ clearPollInterval();
1522
+ if (!cartInvalidatedRef.current) {
1523
+ cartInvalidatedRef.current = true;
1524
+ await queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
1525
+ }
1526
+ setPhase('success');
1527
+ return;
1528
+ }
1529
+
1530
+ if (st != null && TERMINAL_FAILURE_STATUSES.has(st)) {
1531
+ clearPollInterval();
1532
+ setErrorMessage('Ödeme tamamlanamadı veya iptal edildi.');
1533
+ setPhase('error');
1534
+ }
1535
+ } catch {
1536
+ pollFailCountRef.current += 1;
1537
+ if (pollFailCountRef.current >= 5 && !cancelled) {
1538
+ clearPollInterval();
1539
+ setErrorMessage('Ödeme durumu alınamadı. Lütfen daha sonra siparişlerinizi kontrol edin.');
1540
+ setPhase('error');
1541
+ }
1542
+ }
1543
+ };
1544
+
1545
+ void tick();
1546
+ intervalRef.current = setInterval(() => void tick(), 3000);
1547
+
1548
+ return () => {
1549
+ cancelled = true;
1550
+ clearPollInterval();
1551
+ };
1552
+ }, [isSuccess, transactionId, orderIdParam, clearPollInterval, queryClient]);
1553
+
1554
+ useEffect(() => {
1555
+ if (phase !== 'success' || cartInvalidatedRef.current) return;
1556
+ cartInvalidatedRef.current = true;
1557
+ void queryClient.invalidateQueries({ queryKey: ['organization', 'cart'] });
1558
+ }, [phase, queryClient]);
1559
+
1560
+ const displayTitle =
1561
+ phase === 'success' ? (titleParam ?? 'Ödeme başarılı') : (titleParam ?? 'Ödeme başarısız');
1562
+
1563
+ const displayMessage =
1564
+ phase === 'success'
1565
+ ? (messageParam ?? 'Ödemeniz alındı. Siparişiniz işleme alınacaktır.')
1566
+ : (errorMessage ?? messageParam ?? 'İşlem tamamlanamadı.');
1567
+
1568
+ const showOrderLoading = phase === 'success' && resolvedOrderId && orderQuery.isLoading;
1569
+
1570
+ return (
1571
+ <div className="min-h-screen bg-gray-50">
1572
+ <CheckoutShellHeader />
1573
+
1574
+ <main className="mx-auto max-w-2xl px-4 py-10 sm:py-14">
1575
+ {(phase === 'loading' || phase === 'polling' || showOrderLoading) && (
1576
+ <div className="rounded-2xl border border-gray-100 bg-white p-10 text-center shadow-sm">
1577
+ <Loader2 className="mx-auto mb-5 h-10 w-10 animate-spin text-[#CF0A2C]" aria-hidden />
1578
+ <h1 className="text-xl font-black uppercase tracking-tighter text-gray-900 mb-3">Ödeme onaylanıyor</h1>
1579
+ <p className="text-sm text-gray-600 leading-relaxed">
1580
+ Siparişiniz oluşturuluyor. Lütfen bu sayfada kalın.
1581
+ </p>
1582
+ </div>
1583
+ )}
1584
+
1585
+ {phase === 'success' && !showOrderLoading && (
1586
+ <div className="space-y-6">
1587
+ <div className="rounded-2xl border border-gray-100 bg-white p-8 sm:p-10 text-center shadow-sm">
1588
+ <div className="mx-auto mb-5 inline-flex h-16 w-16 items-center justify-center rounded-full bg-green-50 text-green-600">
1589
+ <CheckCircle2 size={32} aria-hidden />
1590
+ </div>
1591
+ <h1 className="text-2xl font-black uppercase tracking-tighter text-gray-900 mb-2">{displayTitle}</h1>
1592
+ <p className="text-sm text-gray-600 leading-relaxed">{displayMessage}</p>
1593
+ </div>
1594
+
1595
+ <div className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm space-y-4">
1596
+ <p className="text-xs font-black uppercase tracking-widest text-[#CF0A2C]">İşlem bilgileri</p>
1597
+ <dl className="space-y-3 text-sm">
1598
+ {resolvedPaymentId ? (
1599
+ <div className="flex flex-col gap-0.5 sm:flex-row sm:justify-between sm:gap-4">
1600
+ <dt className="text-gray-500">Ödeme no</dt>
1601
+ <dd className="font-mono font-medium text-gray-900 break-all" title={resolvedPaymentId}>
1602
+ {shortenId(resolvedPaymentId)}
1603
+ </dd>
1604
+ </div>
1605
+ ) : null}
1606
+ {transactionId ? (
1607
+ <div className="flex flex-col gap-0.5 sm:flex-row sm:justify-between sm:gap-4">
1608
+ <dt className="text-gray-500">İşlem no</dt>
1609
+ <dd className="font-mono font-medium text-gray-900 break-all" title={transactionId}>
1610
+ {shortenId(transactionId)}
1611
+ </dd>
1612
+ </div>
1613
+ ) : null}
1614
+ {resolvedOrderId ? (
1615
+ <div className="flex flex-col gap-0.5 sm:flex-row sm:justify-between sm:gap-4">
1616
+ <dt className="text-gray-500">Sipariş no</dt>
1617
+ <dd className="font-medium text-gray-900">
1618
+ {order?.orderNumber ? `#${order.orderNumber}` : shortenId(resolvedOrderId)}
1619
+ </dd>
1620
+ </div>
1621
+ ) : null}
1622
+ {order?.totalPayAmount != null ? (
1623
+ <div className="flex flex-col gap-0.5 sm:flex-row sm:justify-between sm:gap-4 border-t border-gray-100 pt-3">
1624
+ <dt className="text-gray-500">Ödenen tutar</dt>
1625
+ <dd className="font-bold tabular-nums text-[#CF0A2C]">{order.totalPayAmount.toTry()}</dd>
1626
+ </div>
1627
+ ) : null}
1628
+ </dl>
1629
+ </div>
1630
+
1631
+ {order?.items && order.items.length > 0 ? (
1632
+ <div className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm">
1633
+ <p className="text-xs font-black uppercase tracking-widest text-[#CF0A2C] mb-4">Sipariş özeti</p>
1634
+ <ul className="space-y-4">
1635
+ {order.items.map(item => {
1636
+ const imageSrc = item.productImage
1637
+ ? `${process.env.NEXT_PUBLIC_STORAGE_URL}/${item.productImage.replace(/^\//, '')}`
1638
+ : null;
1639
+ return (
1640
+ <li key={item.id} className="flex gap-4">
1641
+ <div className="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl border border-gray-100 bg-gray-50">
1642
+ {imageSrc ? (
1643
+ <img src={imageSrc} alt={item.productName ?? item.title ?? 'Ürün'} className="h-full w-full object-cover" />
1644
+ ) : (
1645
+ <div className="flex h-full w-full items-center justify-center text-gray-300">
1646
+ <Package size={24} />
1647
+ </div>
1648
+ )}
1649
+ <span className="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-[#CF0A2C] text-[10px] font-black text-white">
1650
+ {item.quantity}
1651
+ </span>
1652
+ </div>
1653
+ <div className="min-w-0 flex-1">
1654
+ <p className="text-sm font-medium text-gray-900">{item.productName ?? item.title}</p>
1655
+ {item.unitName ? <p className="text-xs text-gray-500 mt-0.5">{item.unitName}</p> : null}
1656
+ </div>
1657
+ </li>
1658
+ );
1659
+ })}
1660
+ </ul>
1661
+ </div>
1662
+ ) : null}
1663
+
1664
+ <div className="flex flex-col gap-3 sm:flex-row">
1665
+ {resolvedOrderId ? (
1666
+ <Link
1667
+ href={`/hesabim/siparislerim/${resolvedOrderId}`}
1668
+ className="inline-flex flex-1 justify-center rounded-xl bg-[#CF0A2C] px-8 py-3.5 text-sm font-black uppercase tracking-widest text-white transition hover:bg-[#a80824]"
1669
+ >
1670
+ Siparişi görüntüle
1671
+ </Link>
1672
+ ) : null}
1673
+ <Link
1674
+ href="/"
1675
+ className="inline-flex flex-1 justify-center rounded-xl border-2 border-gray-200 px-8 py-3.5 text-sm font-black uppercase tracking-widest text-gray-900 transition hover:border-gray-900"
1676
+ >
1677
+ Alışverişe devam
1678
+ </Link>
1679
+ </div>
1680
+ </div>
1681
+ )}
1682
+
1683
+ {phase === 'error' && (
1684
+ <div className="space-y-6">
1685
+ <div className="rounded-2xl border border-gray-100 bg-white p-8 sm:p-10 text-center shadow-sm">
1686
+ <div className="mx-auto mb-5 inline-flex h-16 w-16 items-center justify-center rounded-full bg-red-50 text-[#CF0A2C]">
1687
+ <XCircle size={32} aria-hidden />
1688
+ </div>
1689
+ <h1 className="text-2xl font-black uppercase tracking-tighter text-gray-900 mb-2">{displayTitle}</h1>
1690
+ <p className="text-sm text-gray-600 leading-relaxed">{displayMessage}</p>
1691
+ </div>
1692
+
1693
+ {(transactionId || resolvedPaymentId) && (
1694
+ <div className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm space-y-3 text-sm">
1695
+ <p className="text-xs font-black uppercase tracking-widest text-gray-500">Referans bilgileri</p>
1696
+ {transactionId ? (
1697
+ <div className="flex flex-col gap-0.5 sm:flex-row sm:justify-between">
1698
+ <span className="text-gray-500">İşlem no</span>
1699
+ <span className="font-mono text-gray-900 break-all" title={transactionId}>
1700
+ {shortenId(transactionId)}
1701
+ </span>
1702
+ </div>
1703
+ ) : null}
1704
+ {resolvedPaymentId ? (
1705
+ <div className="flex flex-col gap-0.5 sm:flex-row sm:justify-between">
1706
+ <span className="text-gray-500">Ödeme no</span>
1707
+ <span className="font-mono text-gray-900 break-all" title={resolvedPaymentId}>
1708
+ {shortenId(resolvedPaymentId)}
1709
+ </span>
1710
+ </div>
1711
+ ) : null}
1712
+ </div>
1713
+ )}
1714
+
1715
+ <div className="flex flex-col gap-3 sm:flex-row">
1716
+ <Link
1717
+ href={webReturnUrl}
1718
+ className="inline-flex flex-1 justify-center rounded-xl bg-[#CF0A2C] px-8 py-3.5 text-sm font-black uppercase tracking-widest text-white transition hover:bg-[#a80824]"
1719
+ >
1720
+ Ödemeyi tekrar dene
1721
+ </Link>
1722
+ <Link
1723
+ href="/sepet"
1724
+ className="inline-flex flex-1 justify-center rounded-xl border-2 border-gray-200 px-8 py-3.5 text-sm font-black uppercase tracking-widest text-gray-900 transition hover:border-gray-900"
1725
+ >
1726
+ Sepete dön
1727
+ </Link>
1728
+ </div>
1729
+ </div>
1730
+ )}
1731
+
1732
+ {!hasValidStatus && (
1733
+ <div className="rounded-2xl border border-gray-100 bg-white p-10 text-center shadow-sm">
1734
+ <h1 className="text-xl font-black uppercase tracking-tighter text-gray-900 mb-3">Geçersiz bağlantı</h1>
1735
+ <p className="text-sm text-gray-600 mb-8 leading-relaxed">
1736
+ Ödeme sonucu bulunamadı. Lütfen ödeme adımından tekrar deneyin.
1737
+ </p>
1738
+ <Link
1739
+ href={webReturnUrl}
1740
+ className="inline-flex justify-center rounded-xl bg-[#CF0A2C] px-8 py-3.5 text-sm font-black uppercase tracking-widest text-white transition hover:bg-[#a80824]"
1741
+ >
1742
+ Ödemeye git
1743
+ </Link>
1744
+ </div>
1745
+ )}
1746
+ </main>
1747
+ </div>
1748
+ );
1749
+ }
1750
+
1751
+ function CheckoutPageInner() {
1752
+ const searchParams = useSearchParams();
1753
+ const type = searchParams.get('type');
1754
+ const statusParam = searchParams.get('status');
1755
+ const token = searchParams.get('token');
1756
+ const providerParam = searchParams.get('provider');
1757
+ const transactionId = searchParams.get('transactionId');
1758
+
1759
+ const isPaymentResult =
1760
+ statusParam === 'success' || statusParam === 'fail' || statusParam === 'error';
1761
+
1762
+ const isPayFlow =
1763
+ type === 'pay' ||
1764
+ Boolean(token) ||
1765
+ (providerParam === 'garanti' && Boolean(transactionId?.trim()));
1766
+
1767
+ if (isPaymentResult) return <CheckoutResultView />;
1768
+ if (isPayFlow) return <CheckoutPayView />;
1769
+ return <CheckoutMainView />;
1770
+ }
1771
+
1772
+ function CheckoutMainView() {
1773
+ const { cart, branch } = useRaxon();
1774
+ const [checkoutStep, setCheckoutStep] = useState<CheckoutStep>('contact');
1775
+ const [confirmedContactEmail, setConfirmedContactEmail] = useState('');
1776
+
1777
+ useEffect(() => {
1778
+ const fromCart = cart?.email?.trim() || '';
1779
+ if (fromCart) setConfirmedContactEmail(fromCart);
1780
+ }, [cart?.email]);
1781
+
1782
+ useEffect(() => {
1783
+ const hasContactEmail = Boolean(cart?.email?.trim() || confirmedContactEmail);
1784
+ if (!hasContactEmail && checkoutStep !== 'contact') {
1785
+ setCheckoutStep('contact');
1786
+ }
1787
+ }, [cart?.email, confirmedContactEmail, checkoutStep]);
1788
+
1789
+ useEffect(() => {
1790
+ const handleLoginSuccess = () => window.location.reload();
1791
+ window.addEventListener(LOGIN_SUCCESS_EVENT, handleLoginSuccess);
1792
+ return () => window.removeEventListener(LOGIN_SUCCESS_EVENT, handleLoginSuccess);
1793
+ }, []);
1794
+
1795
+ return (
1796
+ <div className="min-h-screen overflow-x-hidden bg-gray-50">
1797
+ <header className="border-b border-gray-100 bg-white">
1798
+ <div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-5 sm:px-6 lg:px-8">
1799
+ <Link href="/" className="flex flex-col items-start gap-1">
1800
+ <GeneralImage
1801
+ quality={85}
1802
+ src={branch?.logoMedia?.relativePath ? `${process.env.NEXT_PUBLIC_STORAGE_URL}/${branch.logoMedia.relativePath}` : ''}
1803
+ alt={branch?.tradingName ?? 'Logo'}
1804
+ width={120}
1805
+ height={36}
1806
+ className="h-7 w-auto object-contain"
1807
+ />
1808
+ </Link>
1809
+ <Link
1810
+ href="/sepet"
1811
+ className="text-xs font-black uppercase tracking-widest text-gray-900 transition-colors hover:text-[#CF0A2C]"
1812
+ >
1813
+ Sepete dön
1814
+ </Link>
1815
+ </div>
1816
+ </header>
1817
+
1818
+ <div className="mx-auto max-w-7xl px-4 py-10 sm:px-6 sm:py-14 lg:px-8">
1819
+ <div className="flex flex-col gap-10 md:flex-row md:items-start md:gap-10 lg:gap-14">
1820
+ <div className="min-w-0 flex-1 rounded-2xl border border-gray-100 bg-white p-6 shadow-sm sm:p-8">
1821
+ <nav className="mb-8 flex flex-wrap gap-2" aria-label="Ödeme adımları">
1822
+ {(['contact', 'address', 'checkout'] as CheckoutStep[]).map((step, index) => {
1823
+ const stepOrder = ['contact', 'address', 'checkout'].indexOf(checkoutStep);
1824
+ const isActive = checkoutStep === step;
1825
+ const isDone = index < stepOrder;
1826
+ return (
1827
+ <span
1828
+ key={step}
1829
+ className={`rounded-full px-3 py-1 text-xs font-semibold ${
1830
+ isActive
1831
+ ? 'bg-[#CF0A2C] text-white'
1832
+ : isDone
1833
+ ? 'bg-[#CF0A2C]/10 text-[#CF0A2C]'
1834
+ : 'bg-gray-100 text-gray-500'
1835
+ }`}
1836
+ >
1837
+ {index + 1}. {CHECKOUT_STEP_LABELS[step]}
1838
+ </span>
1839
+ );
1840
+ })}
1841
+ </nav>
1842
+
1843
+ {checkoutStep === 'contact' && (
1844
+ <ViewStep1
1845
+ onContinue={email => {
1846
+ setConfirmedContactEmail(email);
1847
+ setCheckoutStep('address');
1848
+ }}
1849
+ />
1850
+ )}
1851
+ {checkoutStep === 'address' && (
1852
+ <ViewStep2 onComplete={() => setCheckoutStep('checkout')} onBack={() => setCheckoutStep('contact')} />
1853
+ )}
1854
+ {checkoutStep === 'checkout' && <ViewStepCheckout onBack={() => setCheckoutStep('address')} />}
1855
+ </div>
1856
+
1857
+ <aside className="w-full shrink-0 md:w-96 md:sticky md:top-24 md:self-start">
1858
+ <div className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm lg:p-8">
1859
+ <span className="text-sm font-black text-[#CF0A2C] uppercase tracking-widest">Özet</span>
1860
+ <h2 className="mt-2 text-xl font-black text-gray-900 uppercase tracking-tighter">Sipariş özeti</h2>
1861
+ <div className="my-6 space-y-5">
1862
+ {cart?.items?.map(item => {
1863
+ const linePay = item.linePay ?? 0;
1864
+ const listGross = (item.lineTotal ?? 0) + (item.lineTax ?? 0);
1865
+ const showListStrike = listGross - linePay > 0.01;
1866
+ const variantLine = formatBasketItemVariantLine(item);
1867
+ return (
1868
+ <div key={item.id} className="flex gap-4">
1869
+ <div className="relative shrink-0">
1870
+ <img
1871
+ src={item.images?.[0] ? `${process.env.NEXT_PUBLIC_STORAGE_URL}/${item.images[0]}` : 'https://placehold.co/80x80/f3f4f6/9ca3af?text=Ürün'}
1872
+ alt={item.product.name}
1873
+ className="h-20 w-20 rounded-xl border border-gray-100 object-cover"
1874
+ />
1875
+ <div className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-[#CF0A2C] text-[11px] font-black text-white shadow-sm">
1876
+ {item.quantity}
1877
+ </div>
1878
+ </div>
1879
+ <div className="min-w-0 flex-1">
1880
+ <h3 className="text-sm font-medium text-gray-900">{item.product.name}</h3>
1881
+ {variantLine ? (
1882
+ <p className="mt-1 text-xs text-gray-500">{variantLine}</p>
1883
+ ) : null}
1884
+ <div className="mt-2 flex flex-wrap items-center gap-2">
1885
+ {showListStrike ? (
1886
+ <span className="text-xs text-gray-400 line-through tabular-nums">{listGross.toTry()}</span>
1887
+ ) : null}
1888
+ <span className="text-sm font-bold tabular-nums text-[#CF0A2C]">{linePay.toTry()}</span>
1889
+ </div>
1890
+ </div>
1891
+ </div>
1892
+ );
1893
+ })}
1894
+ </div>
1895
+ <div className="space-y-3 border-t border-gray-100 pt-5">
1896
+ <div className="flex items-center justify-between text-sm">
1897
+ <span className="flex items-center gap-1 text-gray-600">
1898
+ Ara toplam
1899
+ <HelpCircle className="h-4 w-4 text-gray-400" />
1900
+ </span>
1901
+ <span className="font-semibold tabular-nums text-gray-900">
1902
+ {(cart?.info?.basePrice?.total ?? 0).toTry()}
1903
+ </span>
1904
+ </div>
1905
+ {cart?.info?.tax && cart.info.tax.length > 0 ? (
1906
+ <div className="flex items-center justify-between text-sm">
1907
+ <span className="text-gray-600">KDV</span>
1908
+ <span className="font-semibold tabular-nums text-gray-900">
1909
+ {cart.info.tax.reduce((acc, curr) => acc + curr.tax, 0).toTry()}
1910
+ </span>
1911
+ </div>
1912
+ ) : null}
1913
+ {(cart?.info?.delivery?.pay ?? 0) > 0 ? (
1914
+ <div className="flex items-center justify-between text-sm">
1915
+ <span className="text-gray-600">Kargo</span>
1916
+ <span className="font-semibold tabular-nums text-gray-900">{cart?.info?.delivery?.pay?.toTry()}</span>
1917
+ </div>
1918
+ ) : null}
1919
+ {(cart?.info?.discount?.pay ?? 0) > 0 ? (
1920
+ <div className="flex items-center justify-between text-sm">
1921
+ <span className="font-bold text-[#CF0A2C]">İndirim</span>
1922
+ <span className="font-semibold tabular-nums text-[#CF0A2C]">-{cart?.info?.discount?.pay?.toTry()}</span>
1923
+ </div>
1924
+ ) : null}
1925
+ <div className="flex flex-col gap-2 pt-1">
1926
+ <CartPromoCodeSection variant="checkout" />
1927
+ <button type="button" className="text-left text-xs font-black uppercase tracking-widest text-gray-600 underline-offset-2 transition-colors hover:text-[#CF0A2C] hover:underline">
1928
+ Sipariş notu ekle
1929
+ </button>
1930
+ </div>
1931
+ <div className="flex items-center justify-between border-t border-gray-100 pt-5">
1932
+ <span className="text-sm font-black uppercase tracking-widest text-gray-900">Toplam</span>
1933
+ <span className="text-xl font-black tabular-nums text-[#CF0A2C]">
1934
+ {(cart?.info?.payPrice?.pay ?? 0).toTry()}
1935
+ </span>
1936
+ </div>
1937
+ </div>
1938
+ </div>
1939
+ </aside>
1940
+ </div>
1941
+ </div>
1942
+ </div>
1943
+ );
1944
+ }
1945
+
1946
+ function CheckoutPageFallback() {
1947
+ return (
1948
+ <div className="min-h-screen bg-gray-50 flex items-center justify-center">
1949
+ <Loader2 className="h-10 w-10 animate-spin text-[#CF0A2C]" aria-label="Yükleniyor" />
1950
+ </div>
1951
+ );
1952
+ }
1953
+
1954
+ export default function PaymentPage({ webReturnUrl = '/sepet/odeme' }: CheckoutViewProps) {
1955
+ return (
1956
+ <CheckoutViewContext.Provider value={{ webReturnUrl }}>
1957
+ <Suspense fallback={<CheckoutPageFallback />}>
1958
+ <CheckoutPageInner />
1959
+ </Suspense>
1960
+ </CheckoutViewContext.Provider>
1961
+ );
1962
+ }
1963
+
1964
+ export { PaymentPage as CheckoutView };