@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.
- package/core/component/general.image.tsx +86 -0
- package/core/context/cart.context.tsx +446 -0
- package/core/context/security.context.tsx +151 -0
- package/core/feature/address/api/places.api.ts +76 -0
- package/core/feature/address/form/address-search-input.tsx +125 -0
- package/core/feature/address/hook/use.addres.tsx +63 -0
- package/core/feature/address/hook/use.address-autocomplete.ts +116 -0
- package/core/feature/address/util/address.types.ts +38 -0
- package/core/feature/address/util/parse-google-place.ts +66 -0
- package/core/feature/analytic-event/analytic.event.api.ts +27 -0
- package/core/feature/analytic-event/analytic.event.context.tsx +180 -0
- package/core/feature/analytic-event/analytic.event.util.ts +42 -0
- package/core/feature/analytic-event/use.analytic.auto.tsx +114 -0
- package/core/feature/article/hook/use.article.tsx +33 -0
- package/core/feature/attribute/hook/use.attribute.tsx +24 -0
- package/core/feature/auth/hook/use.auth.tsx +141 -0
- package/core/feature/auth/modal/modal.auth.tsx +80 -0
- package/core/feature/auth/view/view.login.tsx +199 -0
- package/core/feature/auth/view/view.register.tsx +333 -0
- package/core/feature/bank-account/hook/use.bank.account.tsx +47 -0
- package/core/feature/brand/hook/use.brand.tsx +24 -0
- package/core/feature/cart/component/cart.order.summary.tsx +89 -0
- package/core/feature/cart/component/cart.promo.code.section.tsx +208 -0
- package/core/feature/cart/hook/use.cart.tsx +267 -0
- package/core/feature/cart/util/basket-pay.response.ts +67 -0
- package/core/feature/cart/util/cart-optimistic.ts +425 -0
- package/core/feature/cart/util/garanti-payment.ts +27 -0
- package/core/feature/collection/hook/use.collection.tsx +32 -0
- package/core/feature/delivery-method/hook/use.delivery.method.tsx +40 -0
- package/core/feature/delivery-method/util/checkout.delivery.method.ts +11 -0
- package/core/feature/faq/hook/use.faq.tsx +23 -0
- package/core/feature/favorite/hook/use.favorite.tsx +48 -0
- package/core/feature/form-submit/form/form.contact.tsx +118 -0
- package/core/feature/form-submit/hook/use.form.submit.tsx +16 -0
- package/core/feature/invoice/hook/use.invoice.tsx +51 -0
- package/core/feature/newsletter/hook/use.newsletter.tsx +124 -0
- package/core/feature/newsletter/modal/modal.newsletter.product.tsx +163 -0
- package/core/feature/order/hook/use.order.tsx +31 -0
- package/core/feature/payment-method/checkout.payment.options.ts +117 -0
- package/core/feature/payment-method/hook/use.payment.method.tsx +44 -0
- package/core/feature/product/hook/use.product.tsx +122 -0
- package/core/feature/profile/hook/use.profile.tsx +126 -0
- package/core/feature/promo-code/hook/use.promo.code.tsx +27 -0
- package/core/interface/basket.interface.ts +360 -0
- package/core/interface/bootstrap.interface.ts +39 -0
- package/core/interface/context.interface.ts +9 -0
- package/core/interface/inventory.interface.ts +88 -0
- package/core/interface/nexine.interface.ts +4 -0
- package/core/interface/prisma.interface.ts +8844 -0
- package/core/interface/product.interface.ts +111 -0
- package/core/raxon.context.tsx +256 -0
- package/core/schema/checkout.schema.ts +103 -0
- package/core/util/basket.item.display.ts +19 -0
- package/core/util/category.nav.ts +46 -0
- package/core/util/client-ip.ts +35 -0
- package/core/util/collection.util.ts +433 -0
- package/core/util/fetch.bootstrap.ts +21 -0
- package/core/util/garanti-payment.ts +5 -0
- package/core/util/nexine.axios.tsx +104 -0
- package/core/util/no-cache.ts +6 -0
- package/core/util/util.ts +191 -0
- package/core/view/view.checkout.tsx +1964 -0
- package/dist/core/view/view.checkout.js +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -3
- 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 "Siparişi tamamla" 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 };
|