Added order confirmation after checkout completion.
Note: This requires MANAGE_ORDERS permission which currently
has the same bug as HANDLE_PAYMENTS. The try-catch ensures
checkout won't fail if confirmation fails. Orders will be
UNCONFIRMED until manually confirmed in dashboard.
The transaction creation was failing due to HANDLE_PAYMENTS permission issues.
Removed the code to get checkout working again. Will implement via
order metadata or core-extensions webhook instead.
- Add CHECKOUT_LANGUAGE_CODE_UPDATE mutation to update checkout language
- Call language code update before completing checkout
- Language code (SR, EN, DE, FR) is now set on checkout before order creation
- This ensures order confirmation emails are sent in the customer's language
- Update step numbering in checkout flow (now 6 steps total)
- Remove CREATE_TRANSACTION_MUTATION call that's causing 400 error
- Saleor's checkoutComplete already creates order with NOT_CHARGED status for COD
- Order doesn't need manual transaction - staff handles fulfillment in dashboard
- Keep payment method selection and UI components intact
- Add green trust badge with checkmark icon above 'Complete Order' button
- Add translations for all locales (EN, SR, DE, FR)
- Badge includes: '30-Day Money-Back Guarantee' text
- Styled with green background and border to match trust/conversion theme
Adds userLanguage and userLocale to checkout metadata during order completion.
This allows N8N workflows to detect the customer's selected language and
send order confirmation emails in the correct language (sr, en, de, fr).
Move analytics tracking inside ORDER_CONFIRMED conditional block
so revenue is only tracked once when order is confirmed, not twice
(once for ORDER_CREATED and once for ORDER_CONFIRMED).
Create service-oriented architecture for better maintainability:
- AnalyticsService: Centralized analytics tracking with OpenPanel
- trackOrderReceived(), trackRevenue(), track()
- Error handling that doesn't break main flow
- Singleton pattern for single instance
- OrderNotificationService: Encapsulates all order email logic
- sendOrderConfirmation() - customer + admin
- sendOrderShipped() - with tracking info
- sendOrderCancelled() - with reason
- sendOrderPaid() - payment confirmation
- Translation logic moved from webhook to service
- Email formatting utilities encapsulated
- Webhook route refactored:
- Reduced from 605 lines to ~250 lines
- No business logic - only HTTP handling
- Delegates to services for emails and analytics
- Cleaner separation of concerns
- New utils file: formatPrice() shared between services
This prevents future bugs by:
1. Centralizing email logic in one place
2. Making code testable (services can be unit tested)
3. Easier to add new webhook handlers
4. Translation logic not mixed with HTTP code
5. Analytics failures don't break order processing
Add OpenPanel import and initialization that was missing from webhook route.
Add order_received and revenue tracking when orders are confirmed.
Revenue tracking uses op.revenue() method with amount, currency,
order_id, and order_number properties.
Saleor stores amounts as actual currency values (e.g., 5479 RSD),
not as cents (e.g., 547900). The formatPrice function was incorrectly
dividing by 100, causing prices like 5479 RSD to display as 55 RSD.
Saleor stores amounts as actual currency values (e.g., 5479 RSD),
not as cents (e.g., 547900). The formatPrice function was incorrectly
dividing by 100, causing prices like 5479 RSD to display as 55 RSD.
- Add /api/op/[...path] proxy route to forward events to self-hosted OpenPanel
- Add scriptUrl=/api/op/op1.js to OpenPanelComponent
- Proxy prevents ad blockers from blocking tracking requests
Both ORDER_CREATED and ORDER_CONFIRMED were sending customer emails,
causing duplicates. Now only ORDER_CONFIRMED sends customer emails,
while both events still notify admins.
Saleor webhook payload stores currency in channel.currency_code,
not as a top-level currency field. Updated interfaces and conversion
function to use the correct location.
Saleor sends webhook payloads as arrays with snake_case fields:
- user_email instead of userEmail
- billing_address instead of billingAddress
- total_gross_amount instead of total.gross.amount
- etc.
Added convertPayloadToOrder() function to transform snake_case payload
to camelCase format expected by our email templates.
Saleor sends event names in lowercase (order_created) but our code
expects uppercase (ORDER_CREATED). This fix normalizes the event
name before checking supported events.
- Add Resend email integration with @react-email/render
- Create email templates: OrderConfirmation, OrderShipped, OrderCancelled, OrderPaid
- Implement webhook handler for ORDER_CREATED and other events
- Add multi-language support for customer emails
- Admin emails in English with order details
- Update checkout page with auto-scroll on order completion
- Configure DASHBOARD_URL environment variable
- Add email field (required) for order confirmation
- Add phone field in contact info section
- Add country dropdown with regional options
- Add validation for email format and required fields
- Add checkoutEmailUpdate mutation call before completing
- Use selected country instead of hardcoded RS
- Add translations for new fields (EN, SR, DE, FR)
- Add NEXT_PUBLIC_SITE_URL to .env.local
- Update email templates to accept siteUrl prop
- Update webhook handler to pass siteUrl from env var
- Update create-webhooks.graphql with placeholder URL
- Add Resend SDK for transactional emails
- Create React Email templates for order events:
- OrderConfirmation
- OrderShipped
- OrderCancelled
- OrderPaid
- Multi-language support (SR, EN, DE, FR)
- Customer emails in their language
- Admin emails in English to me@hytham.me and tamara@hytham.me
- Webhook handler at /api/webhooks/saleor
- Supports: ORDER_CONFIRMED, ORDER_FULLY_PAID, ORDER_CANCELLED, ORDER_FULFILLED
- Add GraphQL mutation to create webhooks in Saleor
- Add Resend API key to .env.local
- Created BundleSelector component for selecting bundle options
- Updated ProductDetail to show bundle options
- Added bundle translations for all 4 locales
- Added GraphQL query for bundle products
- Updated TypeScript types for attributes
- Saleor backend: created bundle products for all base products
Created centralized helpers:
- src/lib/i18n/pageMetadata.ts: All page metadata (titles, descriptions, alt text)
- src/lib/i18n/productText.ts: Product-specific translated text (shortDescription, benefits)
- src/lib/i18n/metadata.ts: Helper functions for locale handling
Updated all pages to use centralized metadata:
- Homepage: Uses getPageMetadata for title, description, productionAlt
- Products page: Uses getPageMetadata
- Product detail: Uses getPageMetadata + getTranslatedShortDescription/getTranslatedBenefits
- About page: Uses getPageMetadata
ProductDetail component now uses:
- getTranslatedShortDescription() instead of locale comparison
- getTranslatedBenefits() instead of locale comparison
All user-facing text now goes through translation files or centralized helpers.
Adding a new language now requires only:
1. Add to SUPPORTED_LOCALES in locales.ts
2. Add LOCALE_CONFIG entry
3. Add entries to pageMetadata.ts and productText.ts
4. Add translation keys to message files
- Added src/lib/i18n/metadata.ts with helper functions
- Updated [locale]/layout.tsx to use DEFAULT_LOCALE constant
- routing.ts already uses centralized SUPPORTED_LOCALES
Note: For full antifragility, a larger refactor would centralize
all hardcoded locale comparisons for metadata text fallbacks.
Currently adding a new language requires:
1. SUPPORTED_LOCALES in locales.ts
2. LOCALE_CONFIG entry
3. Translation keys in all message files
- homepage page.tsx was passing productLocale (SR/EN) instead of locale (sr/en) to ProductCard
- ProductShowcase default locale was "SR" instead of "sr"
- ProductBenefits default locale was "SR" instead of "sr"
These caused URLs like /en/SR/products/... when clicking products
Created src/lib/i18n/locales.ts as single source of truth for:
- SUPPORTED_LOCALES array
- LOCALE_COOKIE name
- DEFAULT_LOCALE
- LOCALE_CONFIG (labels, flags, Saleor locale mapping)
- Helper functions (isValidLocale, getSaleorLocale, getLocaleFromPath)
Updated all files to use centralized constants:
- middleware.ts
- Header.tsx
- ProductCard.tsx
- sitemap.ts
- root layout and locale layout
- routing.ts
Benefits:
- Adding new locale only requires updating ONE file (locales.ts)
- No more hardcoded locale lists scattered across codebase
- Cookie name defined in one place
- Type-safe locale validation
- Added hreflang alternates to root layout for all locales (sr, en, de, fr)
- Added hreflang alternates to [locale] layout for all locales
- Updated sitemap to include all locale variants for every page
- Google will now properly index all language versions
Changes:
- Root page.tsx now detects browser language (Accept-Language header)
and cookie preference to redirect to correct locale (/sr or /en)
- Middleware handles old Serbian URLs (/products, /about, etc.) with
301 redirects to /sr/* while respecting locale cookie
- Header component now includes language switcher dropdown with
flag icons (Serbian and English)
- Language selection sets NEXT_LOCALE cookie and persists preference
Behavior:
- User visits / → redirects to /sr or /en based on cookie/browser
- User selects English from dropdown → cookie set, redirects to /en/*
- User visits /products with en cookie → /en/products (301)
- User visits /products with sr/no cookie → /sr/products (301)
WARNING: This change breaks existing SEO URLs for Serbian locale.
Changes:
- Migrated from separate locale folders (src/app/en/, src/app/de/, etc.)
to [locale] dynamic segments (src/app/[locale]/)
- Serbian is now at /sr/ instead of / (root)
- English at /en/, German at /de/, French at /fr/
- All components updated to generate locale-aware links
- Root / now redirects to /sr (307 temporary redirect)
SEO Impact:
- Previously indexed Serbian URLs (/, /products, /about, /contact)
will now return 404 or redirect to /sr/* URLs
- This is a breaking change for SEO - Serbian pages should ideally
remain at root (/) with only non-default locales getting prefix
- Consider implementing 301 redirects from old URLs to maintain
search engine rankings
Technical Notes:
- next-intl v4 with [locale] structure requires ALL locales to
have the prefix (cannot have default locale at root)
- Alternative approach would be separate folder structure per locale