@akinon/projectzero 1.99.0-rc.67 → 1.99.0-rc.69

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @akinon/projectzero
2
2
 
3
+ ## 1.99.0-rc.69
4
+
5
+ ## 1.99.0-rc.68
6
+
3
7
  ## 1.99.0-rc.67
4
8
 
5
9
  ## 1.99.0-rc.66
@@ -0,0 +1,749 @@
1
+ ---
2
+ applyTo: 'src/app/[commerce]/[locale]/[currency]/account/**'
3
+ ---
4
+
5
+ # Account Pages Development Instructions
6
+
7
+ ## Overview
8
+
9
+ Account pages in Project Zero Next.js provide e-commerce account functionality using Next.js 14 App Router structure. These instructions apply to files in the `src/app/[commerce]/[locale]/[currency]/account/` directory.
10
+
11
+ ## App Router Page Structure
12
+
13
+ ### Basic Page Pattern
14
+
15
+ ```tsx
16
+ // src/app/[commerce]/[locale]/[currency]/account/page.tsx
17
+ import { Metadata } from 'next';
18
+
19
+ export const metadata: Metadata = {
20
+ title: 'Account | Project Zero',
21
+ description: 'User account page'
22
+ };
23
+
24
+ interface Props {
25
+ params: {
26
+ commerce: string;
27
+ locale: string;
28
+ currency: string;
29
+ };
30
+ }
31
+
32
+ export default function AccountPage({ params }: Props) {
33
+ return (
34
+ <div>
35
+ <h1>Account Page</h1>
36
+ {/* Page content */}
37
+ </div>
38
+ );
39
+ }
40
+ ```
41
+
42
+ ### Layout Pattern
43
+
44
+ ```tsx
45
+ // src/app/[commerce]/[locale]/[currency]/account/layout.tsx
46
+ interface Props {
47
+ children: React.ReactNode;
48
+ params: {
49
+ commerce: string;
50
+ locale: string;
51
+ currency: string;
52
+ };
53
+ }
54
+
55
+ export default function AccountLayout({ children, params }: Props) {
56
+ return <div className="account-layout">{children}</div>;
57
+ }
58
+ ```
59
+
60
+ ## Component Import Patterns
61
+
62
+ ### Theme Components
63
+
64
+ ```tsx
65
+ import { Button, Input, Card } from '@theme/components';
66
+ ```
67
+
68
+ ### Akinon Next Components
69
+
70
+ ```tsx
71
+ import { Image } from '@akinon/next/components/image';
72
+ import { Link } from '@akinon/next/components/link';
73
+ ```
74
+
75
+ ## Data Fetching
76
+
77
+ ### Client-Side Hooks
78
+
79
+ Hooks used for fetching account data in Project Zero:
80
+
81
+ ```tsx
82
+ 'use client';
83
+
84
+ import {
85
+ useGetProfileInfoQuery,
86
+ useUpdateProfileMutation,
87
+ useGetOrdersQuery,
88
+ useGetOrderQuery,
89
+ useGetOldOrdersQuery,
90
+ useGetQuotationsQuery,
91
+ useUpdateEmailMutation,
92
+ useUpdatePasswordMutation,
93
+ useSendContactMutation,
94
+ useCancelOrderMutation,
95
+ useBulkCancellationMutation,
96
+ useGetCancellationReasonsQuery,
97
+ useGetContactSubjectsQuery,
98
+ usePasswordResetMutation,
99
+ useGetBasketOffersQuery,
100
+ useGetFutureBasketOffersQuery,
101
+ useGetExpiredBasketOffersQuery,
102
+ useGetDiscountItemsQuery,
103
+ useAnonymizeMutation,
104
+ useGetLoyaltyBalanceQuery,
105
+ useGetLoyaltyTransactionsQuery
106
+ } from '@akinon/next/data/client/account';
107
+
108
+ // User profile information
109
+ const UserProfile = () => {
110
+ const { data: profile, isLoading, error } = useGetProfileInfoQuery();
111
+
112
+ if (isLoading) return <div>Loading...</div>;
113
+ if (error) return <div>Error occurred</div>;
114
+
115
+ return <div>Welcome, {profile?.first_name}</div>;
116
+ };
117
+
118
+ // Orders - with pagination support
119
+ const OrdersList = () => {
120
+ const { data: orders, isLoading } = useGetOrdersQuery({
121
+ page: 1,
122
+ limit: 10
123
+ });
124
+
125
+ return (
126
+ <div>
127
+ <h2>Orders ({orders?.count})</h2>
128
+ {orders?.results?.map((order) => (
129
+ <div key={order.id}>
130
+ <span>Order #{order.number}</span>
131
+ <span>{order.status}</span>
132
+ <span>{order.total} USD</span>
133
+ </div>
134
+ ))}
135
+ </div>
136
+ );
137
+ };
138
+
139
+ // Order detail
140
+ const OrderDetail = ({ orderId }) => {
141
+ const { data: order, isLoading, error } = useGetOrderQuery(orderId);
142
+
143
+ if (isLoading) return <div>Loading order...</div>;
144
+ if (error) return <div>Order not found</div>;
145
+
146
+ return (
147
+ <div>
148
+ <h1>Order #{order?.number}</h1>
149
+ <p>Status: {order?.status}</p>
150
+ <p>Total: {order?.total} USD</p>
151
+ {/* Order products and details */}
152
+ </div>
153
+ );
154
+ };
155
+
156
+ // Old orders
157
+ const OldOrdersList = () => {
158
+ const { data: oldOrders, isLoading } = useGetOldOrdersQuery({
159
+ page: 1,
160
+ limit: 10
161
+ });
162
+
163
+ if (isLoading) return <div>Loading...</div>;
164
+
165
+ return (
166
+ <div>
167
+ <h2>Order History</h2>
168
+ {oldOrders?.results?.map((order) => (
169
+ <div key={order.id}>
170
+ <span>#{order.number}</span>
171
+ <span>{new Date(order.created_date).toLocaleDateString()}</span>
172
+ </div>
173
+ ))}
174
+ </div>
175
+ );
176
+ };
177
+
178
+ // B2B Quotations (if available)
179
+ const QuotationsList = () => {
180
+ const { data: quotations, isLoading } = useGetQuotationsQuery({
181
+ page: 1,
182
+ limit: 10
183
+ });
184
+
185
+ if (isLoading) return <div>Loading quotations...</div>;
186
+
187
+ return (
188
+ <div>
189
+ <h2>Active Quotations</h2>
190
+ {quotations?.results?.map((quotation) => (
191
+ <div key={quotation.id}>
192
+ <h3>{quotation.title}</h3>
193
+ <p>Status: {quotation.status}</p>
194
+ </div>
195
+ ))}
196
+ </div>
197
+ );
198
+ };
199
+
200
+ // Loyalty points (if available)
201
+ const LoyaltyInfo = () => {
202
+ const { data: balance, isLoading: balanceLoading } =
203
+ useGetLoyaltyBalanceQuery();
204
+ const { data: transactions, isLoading: transactionsLoading } =
205
+ useGetLoyaltyTransactionsQuery();
206
+
207
+ if (balanceLoading || transactionsLoading) return <div>Loading...</div>;
208
+
209
+ return (
210
+ <div>
211
+ <h2>Loyalty Points</h2>
212
+ <p>Current Balance: {balance?.balance || 0} points</p>
213
+
214
+ <h3>Recent Transactions</h3>
215
+ <div>
216
+ {transactions?.results?.map((transaction, index) => (
217
+ <div key={index} className="border-b py-2">
218
+ <span>{transaction.amount} points</span>
219
+ <span className="text-gray-500 ml-2">
220
+ {new Date(transaction.created_date).toLocaleDateString()}
221
+ </span>
222
+ </div>
223
+ ))}
224
+ </div>
225
+ </div>
226
+ );
227
+ };
228
+
229
+ // Basket offers
230
+ const BasketOffers = () => {
231
+ const { data: currentOffers } = useGetBasketOffersQuery();
232
+ const { data: futureOffers } = useGetFutureBasketOffersQuery();
233
+ const { data: expiredOffers } = useGetExpiredBasketOffersQuery();
234
+
235
+ return (
236
+ <div>
237
+ <h2>Basket Offers</h2>
238
+
239
+ {currentOffers && (
240
+ <div>
241
+ <h3>Active Offers</h3>
242
+ {/* Offer list */}
243
+ </div>
244
+ )}
245
+
246
+ {futureOffers && (
247
+ <div>
248
+ <h3>Future Offers</h3>
249
+ {/* Future offers */}
250
+ </div>
251
+ )}
252
+ </div>
253
+ );
254
+ };
255
+ ```
256
+
257
+ ### Mutation Hooks - Correct Usage Patterns
258
+
259
+ ```tsx
260
+ 'use client';
261
+
262
+ const ProfileForm = () => {
263
+ const [updateProfile, { isLoading }] = useUpdateProfileMutation();
264
+ const [updateEmail, { isLoading: emailLoading }] = useUpdateEmailMutation();
265
+ const [updatePassword, { isLoading: passwordLoading }] =
266
+ useUpdatePasswordMutation();
267
+
268
+ const handleUpdateProfile = async (formData) => {
269
+ try {
270
+ const result = await updateProfile({
271
+ first_name: formData.firstName,
272
+ last_name: formData.lastName,
273
+ phone: formData.phone,
274
+ date_of_birth: formData.birthDate,
275
+ gender: formData.gender,
276
+ sms_allowed: formData.smsAllowed,
277
+ email_allowed: formData.emailAllowed
278
+ }).unwrap();
279
+
280
+ // Success handling
281
+ console.log('Profile updated:', result);
282
+ } catch (error) {
283
+ // Error handling
284
+ console.error('Profile update error:', error);
285
+ }
286
+ };
287
+
288
+ const handleChangeEmail = async (emailData) => {
289
+ try {
290
+ await updateEmail({
291
+ new_email: emailData.newEmail,
292
+ current_password: emailData.currentPassword
293
+ }).unwrap();
294
+ } catch (error) {
295
+ console.error('Email change error:', error);
296
+ }
297
+ };
298
+
299
+ const handleChangePassword = async (passwordData) => {
300
+ try {
301
+ await updatePassword({
302
+ old_password: passwordData.oldPassword,
303
+ new_password1: passwordData.newPassword,
304
+ new_password2: passwordData.confirmPassword
305
+ }).unwrap();
306
+ } catch (error) {
307
+ console.error('Password change error:', error);
308
+ }
309
+ };
310
+
311
+ return (
312
+ <div className="space-y-6">
313
+ {/* Profile form */}
314
+ <button
315
+ onClick={handleUpdateProfile}
316
+ disabled={isLoading}
317
+ className="bg-blue-500 text-white p-2 rounded disabled:opacity-50"
318
+ >
319
+ {isLoading ? 'Saving...' : 'Update Profile'}
320
+ </button>
321
+ </div>
322
+ );
323
+ };
324
+
325
+ // Order cancellation
326
+ const OrderActions = ({ orderId }) => {
327
+ const [cancelOrder, { isLoading }] = useCancelOrderMutation();
328
+ const { data: cancellationReasons } = useGetCancellationReasonsQuery();
329
+
330
+ const handleCancelOrder = async (reasonId) => {
331
+ try {
332
+ await cancelOrder({
333
+ id: orderId,
334
+ reason: reasonId
335
+ }).unwrap();
336
+
337
+ // Show success message
338
+ } catch (error) {
339
+ console.error('Order cancellation error:', error);
340
+ }
341
+ };
342
+
343
+ return (
344
+ <div>
345
+ <h3>Cancel Order</h3>
346
+ {cancellationReasons?.map((reason) => (
347
+ <button
348
+ key={reason.id}
349
+ onClick={() => handleCancelOrder(reason.id)}
350
+ disabled={isLoading}
351
+ className="block w-full text-left p-2 hover:bg-gray-100 disabled:opacity-50"
352
+ >
353
+ {reason.text}
354
+ </button>
355
+ ))}
356
+ </div>
357
+ );
358
+ };
359
+
360
+ // Contact form
361
+ const ContactForm = () => {
362
+ const [sendContact, { isLoading }] = useSendContactMutation();
363
+ const { data: subjects } = useGetContactSubjectsQuery();
364
+
365
+ const handleSubmit = async (formData) => {
366
+ try {
367
+ const form = new FormData();
368
+ form.append('subject', formData.subject);
369
+ form.append('message', formData.message);
370
+ form.append('order_number', formData.orderNumber || '');
371
+
372
+ if (formData.attachment) {
373
+ form.append('attachment', formData.attachment);
374
+ }
375
+
376
+ await sendContact(form).unwrap();
377
+
378
+ // Success message
379
+ } catch (error) {
380
+ console.error('Contact form submission error:', error);
381
+ }
382
+ };
383
+
384
+ return (
385
+ <form onSubmit={handleSubmit}>
386
+ <select name="subject" required>
387
+ <option value="">Select subject</option>
388
+ {subjects?.map((subject) => (
389
+ <option key={subject.id} value={subject.id}>
390
+ {subject.text}
391
+ </option>
392
+ ))}
393
+ </select>
394
+
395
+ <textarea name="message" required placeholder="Your message" />
396
+
397
+ <input type="file" name="attachment" />
398
+
399
+ <button
400
+ type="submit"
401
+ disabled={isLoading}
402
+ className="bg-blue-500 text-white p-2 rounded disabled:opacity-50"
403
+ >
404
+ {isLoading ? 'Sending...' : 'Send'}
405
+ </button>
406
+ </form>
407
+ );
408
+ };
409
+
410
+ // Account anonymization (GDPR)
411
+ const AccountSettings = () => {
412
+ const [anonymize, { isLoading }] = useAnonymizeMutation();
413
+
414
+ const handleAnonymize = async () => {
415
+ if (confirm('Are you sure you want to permanently delete your account?')) {
416
+ try {
417
+ const result = await anonymize().unwrap();
418
+
419
+ // Redirect user to logout page
420
+ window.location.href = '/logout';
421
+ } catch (error) {
422
+ console.error('Account anonymization error:', error);
423
+ }
424
+ }
425
+ };
426
+
427
+ return (
428
+ <div className="bg-red-50 p-4 rounded border border-red-200">
429
+ <h3 className="text-red-800 font-semibold">Delete Account</h3>
430
+ <p className="text-red-700 text-sm mb-4">
431
+ This action cannot be undone. All your data will be permanently deleted.
432
+ </p>
433
+ <button
434
+ onClick={handleAnonymize}
435
+ disabled={isLoading}
436
+ className="bg-red-500 text-white px-4 py-2 rounded disabled:opacity-50"
437
+ >
438
+ {isLoading ? 'Deleting...' : 'Delete Account'}
439
+ </button>
440
+ </div>
441
+ );
442
+ };
443
+ ```
444
+
445
+ ## State Management
446
+
447
+ ### Redux Store
448
+
449
+ ```tsx
450
+ 'use client';
451
+
452
+ import { useAppSelector, useAppDispatch } from '@akinon/next/redux/store';
453
+
454
+ const AccountComponent = () => {
455
+ const dispatch = useAppDispatch();
456
+ const { user, isLoading } = useAppSelector((state) => state.account);
457
+
458
+ // Redux actions
459
+ const handleUpdate = () => {
460
+ dispatch(/* account action */);
461
+ };
462
+
463
+ return <div>{/* Component */}</div>;
464
+ };
465
+ ```
466
+
467
+ ## Form Handling
468
+
469
+ ### React Hook Form
470
+
471
+ ```tsx
472
+ 'use client';
473
+
474
+ import { useForm } from 'react-hook-form';
475
+ import { yupResolver } from '@hookform/resolvers/yup';
476
+ import * as yup from 'yup';
477
+ import { useLocalization } from '@akinon/next/hooks/useLocalization';
478
+
479
+ const profileSchema = yup.object({
480
+ first_name: yup.string().required('First name is required'),
481
+ last_name: yup.string().required('Last name is required'),
482
+ phone: yup.string().required('Phone is required'),
483
+ date_of_birth: yup.date().nullable(),
484
+ gender: yup.string(),
485
+ sms_allowed: yup.boolean(),
486
+ email_allowed: yup.boolean()
487
+ });
488
+
489
+ const ProfileForm = () => {
490
+ const { t } = useLocalization();
491
+ const [updateProfile, { isLoading }] = useUpdateProfileMutation();
492
+
493
+ const {
494
+ register,
495
+ handleSubmit,
496
+ formState: { errors }
497
+ } = useForm({
498
+ resolver: yupResolver(profileSchema)
499
+ });
500
+
501
+ const onSubmit = async (data) => {
502
+ try {
503
+ await updateProfile(data).unwrap();
504
+ // Success message
505
+ } catch (error) {
506
+ // Error handling
507
+ }
508
+ };
509
+
510
+ return (
511
+ <form onSubmit={handleSubmit(onSubmit)}>
512
+ <input
513
+ {...register('first_name')}
514
+ placeholder="First Name"
515
+ className="border p-2 rounded"
516
+ />
517
+ {errors.first_name && (
518
+ <span className="text-red-500">{errors.first_name.message}</span>
519
+ )}
520
+
521
+ <input
522
+ {...register('last_name')}
523
+ placeholder="Last Name"
524
+ className="border p-2 rounded"
525
+ />
526
+ {errors.last_name && (
527
+ <span className="text-red-500">{errors.last_name.message}</span>
528
+ )}
529
+
530
+ <button
531
+ type="submit"
532
+ disabled={isLoading}
533
+ className="bg-blue-500 text-white p-2 rounded"
534
+ >
535
+ {isLoading ? 'Saving...' : 'Save'}
536
+ </button>
537
+ </form>
538
+ );
539
+ };
540
+ ```
541
+
542
+ ## Styling
543
+
544
+ ### TailwindCSS
545
+
546
+ ```tsx
547
+ const AccountPage = () => {
548
+ return (
549
+ <div className="container mx-auto px-4 py-8">
550
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
551
+ <div className="bg-white rounded-lg shadow p-6">
552
+ <h2 className="text-xl font-semibold mb-4">Profile</h2>
553
+ {/* Content */}
554
+ </div>
555
+ </div>
556
+ </div>
557
+ );
558
+ };
559
+ ```
560
+
561
+ ### Conditional Classes
562
+
563
+ ```tsx
564
+ import { clsx } from 'clsx';
565
+
566
+ const StatusBadge = ({ status }) => {
567
+ return (
568
+ <span
569
+ className={clsx('px-2 py-1 rounded text-sm', {
570
+ 'bg-green-100 text-green-800': status === 'completed',
571
+ 'bg-yellow-100 text-yellow-800': status === 'pending',
572
+ 'bg-red-100 text-red-800': status === 'cancelled'
573
+ })}
574
+ >
575
+ {status}
576
+ </span>
577
+ );
578
+ };
579
+ ```
580
+
581
+ ## Localization
582
+
583
+ ### Usage
584
+
585
+ ```tsx
586
+ import { useLocalization } from '@akinon/next/hooks/useLocalization';
587
+
588
+ const AccountComponent = () => {
589
+ const { t } = useLocalization();
590
+
591
+ return (
592
+ <div>
593
+ <h1>{t('account.title')}</h1>
594
+ <p>{t('account.welcome_message', { name: 'User' })}</p>
595
+ </div>
596
+ );
597
+ };
598
+ ```
599
+
600
+ ## Error Handling
601
+
602
+ ### API Errors
603
+
604
+ ```tsx
605
+ const AccountComponent = () => {
606
+ const { data, error, isLoading } = useGetProfileInfoQuery();
607
+
608
+ if (error) {
609
+ return (
610
+ <div className="bg-red-50 border border-red-200 rounded p-4">
611
+ <p className="text-red-700">
612
+ {error.data?.message || 'An error occurred'}
613
+ </p>
614
+ </div>
615
+ );
616
+ }
617
+
618
+ if (isLoading) {
619
+ return <div className="animate-pulse">Loading...</div>;
620
+ }
621
+
622
+ return <div>{/* Normal content */}</div>;
623
+ };
624
+ ```
625
+
626
+ ## Performance
627
+
628
+ ### Loading States
629
+
630
+ ```tsx
631
+ const LoadingSkeleton = () => (
632
+ <div className="animate-pulse space-y-4">
633
+ <div className="h-4 bg-gray-200 rounded w-3/4"></div>
634
+ <div className="h-4 bg-gray-200 rounded w-1/2"></div>
635
+ <div className="h-32 bg-gray-200 rounded"></div>
636
+ </div>
637
+ );
638
+ ```
639
+
640
+ ### Lazy Loading
641
+
642
+ ```tsx
643
+ import { LazyComponent } from '@akinon/next/components/lazy-component';
644
+
645
+ const LazyOrderHistory = LazyComponent(() => import('./OrderHistory'));
646
+ ```
647
+
648
+ ## Best Practices
649
+
650
+ ### TypeScript
651
+
652
+ ```tsx
653
+ interface User {
654
+ id: number;
655
+ first_name: string;
656
+ last_name: string;
657
+ email: string;
658
+ }
659
+
660
+ interface Order {
661
+ id: number;
662
+ number: string;
663
+ status: string;
664
+ total: number;
665
+ }
666
+
667
+ interface Address {
668
+ id: number;
669
+ title: string;
670
+ address: string;
671
+ city: string;
672
+ district: string;
673
+ }
674
+ ```
675
+
676
+ ### File Organization
677
+
678
+ ```
679
+ account/
680
+ ├── page.tsx # Main account page
681
+ ├── layout.tsx # Layout (optional)
682
+ ├── profile/
683
+ │ └── page.tsx # Profile page
684
+ ├── orders/
685
+ │ ├── page.tsx # Orders list
686
+ │ └── [id]/
687
+ │ └── page.tsx # Order detail
688
+ └── addresses/
689
+ ├── page.tsx # Address list
690
+ └── new/
691
+ └── page.tsx # New address
692
+ ```
693
+
694
+ ---
695
+
696
+ **Note:** These instructions are based on Project Zero's existing API and hook structures. Follow existing patterns when adding new features.
697
+
698
+ ## Important Notes
699
+
700
+ ### Hook Usage Rules
701
+
702
+ 1. **Query hooks** run automatically, no manual triggering required
703
+ 2. **Mutation hooks** return array destructuring with `[mutationFn, { isLoading, error }]`
704
+ 3. Use **unwrap()** for promise-based error handling
705
+ 4. **isLoading** state should be used in UI for loading states
706
+
707
+ ### Error Handling Best Practices
708
+
709
+ ```tsx
710
+ // RTK Query error handling
711
+ const { data, error, isLoading } = useGetOrdersQuery();
712
+
713
+ if (error) {
714
+ // RTK Query error structure
715
+ const errorMessage =
716
+ error.data?.message || error.error || 'An error occurred';
717
+ return <div className="text-red-500">{errorMessage}</div>;
718
+ }
719
+ ```
720
+
721
+ ### Conditional Rendering
722
+
723
+ ```tsx
724
+ // Show alternative content when no data
725
+ const OrdersList = () => {
726
+ const { data: orders, isLoading } = useGetOrdersQuery();
727
+
728
+ if (isLoading) return <LoadingSkeleton />;
729
+
730
+ if (!orders?.results?.length) {
731
+ return (
732
+ <div className="text-center py-8">
733
+ <p>You don't have any orders yet.</p>
734
+ <Link href="/products" className="text-blue-500">
735
+ Start shopping
736
+ </Link>
737
+ </div>
738
+ );
739
+ }
740
+
741
+ return (
742
+ <div>
743
+ {orders.results.map((order) => (
744
+ <OrderCard key={order.id} order={order} />
745
+ ))}
746
+ </div>
747
+ );
748
+ };
749
+ ```
@@ -0,0 +1,73 @@
1
+ ---
2
+ applyTo: '**'
3
+ ---
4
+
5
+ # Edge Cases & Troubleshooting Instructions
6
+
7
+ ## Overview
8
+
9
+ This document contains common edge cases, bugs, and troubleshooting steps encountered in Project Zero Next.js development. These are real-world issues that have caused development delays and should be checked first when similar problems occur.
10
+
11
+ ## Checkout Flow Edge Cases
12
+
13
+ ### 1. Order Selection Page Request Timing
14
+
15
+ **Problem:** `orderselectionpage` request should NOT be called after `walletselectionpage` or `walletpaymentpage` requests.
16
+
17
+ This causes checkout flow to break after wallet payment steps, order selection shows incorrect state, and payment flow gets reset unexpectedly. After `walletselectionpage` or `walletpaymentpage` requests are made, `orderselectionpage` request should not be called. Always check the sequence of checkout API calls.
18
+
19
+ ### 2. Guest Login with URL Parameters
20
+
21
+ **Problem:** Adding `?page=IndexPage` to checkout URLs breaks guest login functionality.
22
+
23
+ Guest login fails on checkout URLs with `?page=IndexPage` but works fine without the parameter. This parameter interferes with authentication middleware routing. Avoid adding unnecessary URL parameters to checkout flows.
24
+
25
+ ### 3. Agreement Error After 3D Payment Redirect
26
+
27
+ **Problem:** "You must accept agreement to continue" error appears after 3D payment or redirections, but the issue is NOT related to agreements.
28
+
29
+ Agreement error occurs after successful 3D payment and checkout flow breaks after external redirections. The error message is misleading - actually `preOrder` data gets reset/lost during payment redirections. Don't assume agreement errors are actually about agreements.
30
+
31
+ ### 4. Address Visibility with fetchCheckout
32
+
33
+ **Problem:** When `fetchCheckout` request includes `page=IndexPage` parameter, addresses don't show in checkout and the entire flow breaks.
34
+
35
+ Addresses are missing from checkout page, shipping options don't load, and checkout flow is completely broken. Avoid adding unnecessary parameters to core checkout requests and validate that all checkout data loads properly.
36
+
37
+ ## Plugin & Configuration Edge Cases
38
+
39
+ ### 5. Attribute-Based Shipping Options Typo
40
+
41
+ **Problem:** `attribute_based_shipping_options` was incorrectly entered as `atribute_based` (missing 't') in brand configurations.
42
+
43
+ Shipping options don't work for specific brands and debugging is difficult because the setting exists but is misspelled. This can cause days of debugging. Always double-check configuration property names.
44
+
45
+ ### 6. Apple Pay Plugin Configuration
46
+
47
+ **Problem:** Apple Pay can work as both `confirmation` and `wallet` payment types, but for the plugin to work properly, it should NOT be configured as `confirmation` type.
48
+
49
+ Apple Pay flow breaks in certain scenarios, payment confirmation issues occur, and plugin conflicts with checkout flow. Always configure Apple Pay as wallet type.
50
+
51
+ ## Build & Development Edge Cases
52
+
53
+ ### 7. Build Failures Due to yarn.lock Corruption
54
+
55
+ **Problem:** Build fails with apparent TypeScript errors in `@akinon/next` components, but the real issue is a corrupted `yarn.lock` file.
56
+
57
+ Build fails with nonsensical type errors, components from `@akinon/next` are flagged with errors, and multiple versions of the same package exist in lock file.
58
+
59
+ ```bash
60
+ # Check for duplicate packages in yarn.lock
61
+ yarn list --pattern "@akinon/next" --depth=0
62
+
63
+ # If multiple versions exist, clean up
64
+ rm yarn.lock
65
+ rm -rf node_modules
66
+ yarn install
67
+ ```
68
+
69
+ Regularly check `yarn.lock` for duplicate dependencies and use `yarn dedupe` after adding new packages.
70
+
71
+ ---
72
+
73
+ **Remember:** Edge cases often have misleading error messages. Always investigate the root cause rather than fixing symptoms. The most time-consuming bugs are usually simple configuration or timing issues.
@@ -1,5 +1,61 @@
1
1
  # projectzeronext
2
2
 
3
+ ## 1.99.0-rc.69
4
+
5
+ ### Minor Changes
6
+
7
+ - 3b255fe: ZERO-3629 :edit warnings in build
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [3b255fe]
12
+ - @akinon/next@1.99.0-rc.69
13
+ - @akinon/pz-akifast@1.99.0-rc.69
14
+ - @akinon/pz-b2b@1.99.0-rc.69
15
+ - @akinon/pz-basket-gift-pack@1.99.0-rc.69
16
+ - @akinon/pz-bkm@1.99.0-rc.69
17
+ - @akinon/pz-checkout-gift-pack@1.99.0-rc.69
18
+ - @akinon/pz-click-collect@1.99.0-rc.69
19
+ - @akinon/pz-credit-payment@1.99.0-rc.69
20
+ - @akinon/pz-gpay@1.99.0-rc.69
21
+ - @akinon/pz-hepsipay@1.99.0-rc.69
22
+ - @akinon/pz-masterpass@1.99.0-rc.69
23
+ - @akinon/pz-one-click-checkout@1.99.0-rc.69
24
+ - @akinon/pz-otp@1.99.0-rc.69
25
+ - @akinon/pz-pay-on-delivery@1.99.0-rc.69
26
+ - @akinon/pz-saved-card@1.99.0-rc.69
27
+ - @akinon/pz-similar-products@1.99.0-rc.69
28
+ - @akinon/pz-tabby-extension@1.99.0-rc.69
29
+ - @akinon/pz-tamara-extension@1.99.0-rc.69
30
+
31
+ ## 1.99.0-rc.68
32
+
33
+ ### Minor Changes
34
+
35
+ - 3d3fe05: ZERO-3639: Add edge cases instruction file
36
+ - fdd9974: ZERO-3636: Add account instruction file
37
+
38
+ ### Patch Changes
39
+
40
+ - @akinon/next@1.99.0-rc.68
41
+ - @akinon/pz-akifast@1.99.0-rc.68
42
+ - @akinon/pz-b2b@1.99.0-rc.68
43
+ - @akinon/pz-basket-gift-pack@1.99.0-rc.68
44
+ - @akinon/pz-bkm@1.99.0-rc.68
45
+ - @akinon/pz-checkout-gift-pack@1.99.0-rc.68
46
+ - @akinon/pz-click-collect@1.99.0-rc.68
47
+ - @akinon/pz-credit-payment@1.99.0-rc.68
48
+ - @akinon/pz-gpay@1.99.0-rc.68
49
+ - @akinon/pz-hepsipay@1.99.0-rc.68
50
+ - @akinon/pz-masterpass@1.99.0-rc.68
51
+ - @akinon/pz-one-click-checkout@1.99.0-rc.68
52
+ - @akinon/pz-otp@1.99.0-rc.68
53
+ - @akinon/pz-pay-on-delivery@1.99.0-rc.68
54
+ - @akinon/pz-saved-card@1.99.0-rc.68
55
+ - @akinon/pz-similar-products@1.99.0-rc.68
56
+ - @akinon/pz-tabby-extension@1.99.0-rc.68
57
+ - @akinon/pz-tamara-extension@1.99.0-rc.68
58
+
3
59
  ## 1.99.0-rc.67
4
60
 
5
61
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectzeronext",
3
- "version": "1.99.0-rc.67",
3
+ "version": "1.99.0-rc.69",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -24,24 +24,24 @@
24
24
  "test:middleware": "jest middleware-matcher.test.ts --bail"
25
25
  },
26
26
  "dependencies": {
27
- "@akinon/next": "1.99.0-rc.67",
28
- "@akinon/pz-akifast": "1.99.0-rc.67",
29
- "@akinon/pz-b2b": "1.99.0-rc.67",
30
- "@akinon/pz-basket-gift-pack": "1.99.0-rc.67",
31
- "@akinon/pz-bkm": "1.99.0-rc.67",
32
- "@akinon/pz-checkout-gift-pack": "1.99.0-rc.67",
33
- "@akinon/pz-click-collect": "1.99.0-rc.67",
34
- "@akinon/pz-credit-payment": "1.99.0-rc.67",
35
- "@akinon/pz-gpay": "1.99.0-rc.67",
36
- "@akinon/pz-hepsipay": "1.99.0-rc.67",
37
- "@akinon/pz-masterpass": "1.99.0-rc.67",
38
- "@akinon/pz-one-click-checkout": "1.99.0-rc.67",
39
- "@akinon/pz-otp": "1.99.0-rc.67",
40
- "@akinon/pz-pay-on-delivery": "1.99.0-rc.67",
41
- "@akinon/pz-saved-card": "1.99.0-rc.67",
42
- "@akinon/pz-similar-products": "1.99.0-rc.67",
43
- "@akinon/pz-tabby-extension": "1.99.0-rc.67",
44
- "@akinon/pz-tamara-extension": "1.99.0-rc.67",
27
+ "@akinon/next": "1.99.0-rc.69",
28
+ "@akinon/pz-akifast": "1.99.0-rc.69",
29
+ "@akinon/pz-b2b": "1.99.0-rc.69",
30
+ "@akinon/pz-basket-gift-pack": "1.99.0-rc.69",
31
+ "@akinon/pz-bkm": "1.99.0-rc.69",
32
+ "@akinon/pz-checkout-gift-pack": "1.99.0-rc.69",
33
+ "@akinon/pz-click-collect": "1.99.0-rc.69",
34
+ "@akinon/pz-credit-payment": "1.99.0-rc.69",
35
+ "@akinon/pz-gpay": "1.99.0-rc.69",
36
+ "@akinon/pz-hepsipay": "1.99.0-rc.69",
37
+ "@akinon/pz-masterpass": "1.99.0-rc.69",
38
+ "@akinon/pz-one-click-checkout": "1.99.0-rc.69",
39
+ "@akinon/pz-otp": "1.99.0-rc.69",
40
+ "@akinon/pz-pay-on-delivery": "1.99.0-rc.69",
41
+ "@akinon/pz-saved-card": "1.99.0-rc.69",
42
+ "@akinon/pz-similar-products": "1.99.0-rc.69",
43
+ "@akinon/pz-tabby-extension": "1.99.0-rc.69",
44
+ "@akinon/pz-tamara-extension": "1.99.0-rc.69",
45
45
  "@hookform/resolvers": "2.9.0",
46
46
  "@next/third-parties": "14.1.0",
47
47
  "@react-google-maps/api": "2.17.1",
@@ -64,7 +64,7 @@
64
64
  "yup": "0.32.11"
65
65
  },
66
66
  "devDependencies": {
67
- "@akinon/eslint-plugin-projectzero": "1.99.0-rc.67",
67
+ "@akinon/eslint-plugin-projectzero": "1.99.0-rc.69",
68
68
  "@semantic-release/changelog": "6.0.2",
69
69
  "@semantic-release/exec": "6.0.3",
70
70
  "@semantic-release/git": "10.0.1",
@@ -30,6 +30,8 @@ describe('Link Component', () => {
30
30
  wrapper = container;
31
31
  });
32
32
 
33
+ wrapper;
34
+
33
35
  it('should render without error', () => {
34
36
  const link = screen.getByRole('link');
35
37
 
@@ -48,7 +48,13 @@ export const Input = forwardRef<
48
48
  props.className
49
49
  );
50
50
 
51
- const inputProps: any = {
51
+ const inputProps: {
52
+ id?: string;
53
+ ref?: Ref<HTMLInputElement>;
54
+ className?: string;
55
+ onFocus?: () => void;
56
+ onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
57
+ } = {
52
58
  id,
53
59
  ref,
54
60
  className: inputClass,
@@ -125,6 +125,7 @@ export const Pagination = (props: PaginationProps) => {
125
125
  setPrevPage(1);
126
126
  setNextPage(1);
127
127
  }
128
+ // eslint-disable-next-line react-hooks/exhaustive-deps
128
129
  }, [page]);
129
130
 
130
131
  useEffect(() => {
@@ -111,7 +111,7 @@ const ContactForm = () => {
111
111
  resolver: yupResolver(contactFormSchema(t))
112
112
  });
113
113
 
114
- const onSubmit: SubmitHandler<ContactFormType> = (data, event) => {
114
+ const onSubmit: SubmitHandler<ContactFormType> = (data) => {
115
115
  const formData = new FormData();
116
116
 
117
117
  Object.keys(data ?? {}).forEach((key) => {
@@ -3,6 +3,7 @@
3
3
  import { Accordion, LoaderSpinner, TabPanel, Tabs } from '@theme/components';
4
4
  import React from 'react';
5
5
  import { useGetWidgetQuery } from '@akinon/next/data/client/misc';
6
+ import { useLocalization } from '@akinon/next/hooks';
6
7
 
7
8
  interface Props {
8
9
  searchKey?: string;
@@ -11,6 +12,7 @@ interface Props {
11
12
  export function FaqTabs(props: Props) {
12
13
  const { searchKey } = props;
13
14
  const { data, isLoading } = useGetWidgetQuery('faq');
15
+ const { locale } = useLocalization();
14
16
 
15
17
  if (isLoading) {
16
18
  return <LoaderSpinner className="mt-4" />;
@@ -29,8 +31,12 @@ export function FaqTabs(props: Props) {
29
31
  {data?.attributes?.faq_contents
30
32
  ?.filter(
31
33
  (faq) =>
32
- faq.value.content.toLocaleLowerCase().includes(searchKey) ||
33
- faq.value.title.toLocaleLowerCase().includes(searchKey)
34
+ faq.value.content
35
+ .toLocaleLowerCase(locale)
36
+ .includes(searchKey) ||
37
+ faq.value.title
38
+ .toLocaleLowerCase(locale)
39
+ .includes(searchKey)
34
40
  )
35
41
  .map((faq, index) => {
36
42
  if (faq.value.category == item.value.category_id) {
@@ -40,7 +40,12 @@ export const BasketItem = (props: Props) => {
40
40
  quantity: number,
41
41
  attributes: object = {}
42
42
  ) => {
43
- const requestParams: any = {
43
+ const requestParams: {
44
+ product: number;
45
+ quantity: number;
46
+ attributes: object;
47
+ namespace?: string;
48
+ } = {
44
49
  product: productPk,
45
50
  quantity,
46
51
  attributes
@@ -12,7 +12,7 @@ export interface BreadcrumbProps {
12
12
  }
13
13
 
14
14
  export default function Breadcrumb(props: BreadcrumbProps) {
15
- const { t } = useLocalization();
15
+ const { t, locale } = useLocalization();
16
16
  const { breadcrumbList = [] } = props;
17
17
 
18
18
  const list = [
@@ -28,7 +28,7 @@ export default function Breadcrumb(props: BreadcrumbProps) {
28
28
  {list.map((item, index) => (
29
29
  <Fragment key={index}>
30
30
  <Link href={item.url}>
31
- {capitalize(item.text.toLocaleLowerCase())}
31
+ {capitalize(item.text.toLocaleLowerCase(locale))}
32
32
  </Link>
33
33
  {index !== list.length - 1 && <Icon name="chevron-end" size={8} />}
34
34
  </Fragment>
@@ -57,6 +57,7 @@ export default function ListPage(props: ListPageProps) {
57
57
  newUrl.searchParams.delete('page');
58
58
  router.push(newUrl.pathname + newUrl.search, undefined);
59
59
  }
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
61
  }, [searchParams, data.products, page]);
61
62
 
62
63
  const { t } = useLocalization();
@@ -6,7 +6,7 @@ import { useLocalization } from '@akinon/next/hooks';
6
6
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
7
7
  import { resetSelectedFacets } from '@theme/redux/reducers/category';
8
8
  import CategoryActiveFilters from '@theme/views/category/category-active-filters';
9
- import { useMemo, useState, useTransition } from 'react';
9
+ import { useMemo, useTransition } from 'react';
10
10
  import { FilterItem } from './filter-item';
11
11
 
12
12
  interface Props {
@@ -58,6 +58,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
58
58
  if (isVariantLoading) {
59
59
  clearProductError();
60
60
  }
61
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
62
  }, [isVariantLoading]);
62
63
 
63
64
  useEffect(() => {
@@ -2,6 +2,7 @@ import 'server-only';
2
2
 
3
3
  import { Link, Accordion } from '@theme/components';
4
4
  import { getWidgetData } from '@akinon/next/data/server';
5
+ import { ServerVariables } from '@akinon/next/utils/server-variables';
5
6
 
6
7
  type SideItem = {
7
8
  value: string;
@@ -47,6 +48,7 @@ type FooterMenuType = {
47
48
 
48
49
  export default async function FooterMenu() {
49
50
  const data = await getWidgetData<FooterMenuType>({ slug: 'footer-menu' });
51
+ const { locale } = ServerVariables;
50
52
 
51
53
  return (
52
54
  <div className="flex-1">
@@ -72,7 +74,7 @@ export default async function FooterMenu() {
72
74
  : '_self'
73
75
  }
74
76
  data-testid={`footer-categories-${item?.value?.name
75
- ?.toLocaleLowerCase()
77
+ ?.toLocaleLowerCase(locale)
76
78
  .split(' ')
77
79
  .join('')}`}
78
80
  >
@@ -96,7 +98,9 @@ export default async function FooterMenu() {
96
98
  ? '_blank'
97
99
  : '_self'
98
100
  }
99
- data-testid={`footer-categories-${item?.value?.name?.toLocaleLowerCase()}`}
101
+ data-testid={`footer-categories-${item?.value?.name?.toLocaleLowerCase(
102
+ locale
103
+ )}`}
100
104
  >
101
105
  {item?.value?.name}
102
106
  </Link>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "1.99.0-rc.67",
3
+ "version": "1.99.0-rc.69",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {