Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"dependencies": {
"@next/third-parties": "^16.1.6",
"@sentry/nextjs": "^10.38.0",
"@spree/next": "^0.6.1",
"@spree/sdk": "^0.6.0",
"@spree/next": "^0.6.4",
"@spree/sdk": "^0.6.3",
"@stripe/react-stripe-js": "^5.6.0",
"@stripe/stripe-js": "^8.7.0",
"next": "^16",
Expand Down
29 changes: 18 additions & 11 deletions src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import {
updateOrderAddresses,
} from "@/lib/data/checkout";
import { isAuthenticated as checkAuth } from "@/lib/data/cookies";
import { getCountries, getCountry } from "@/lib/data/countries";
import { getCountry } from "@/lib/data/countries";
import { getMarketCountries, resolveMarket } from "@/lib/data/markets";
import {
completeCheckoutOrder,
completeCheckoutPaymentSession,
Expand Down Expand Up @@ -95,7 +96,7 @@ function CheckoutSidebar({

export default function CheckoutPage({ params }: CheckoutPageProps) {
// use() must be called before all other hooks to avoid hook order issues
const { id: orderId } = use(params);
const { id: orderId, country: urlCountry } = use(params);
const router = useRouter();
const pathname = usePathname();
const basePath = extractBasePath(pathname);
Expand Down Expand Up @@ -176,19 +177,25 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
handleRemoveCoupon,
]);

// Load order and countries
// Load order and market-scoped countries
const loadOrder = useCallback(async () => {
setLoading(true);
setError(null);

try {
const [orderData, countriesData, addressesData, authStatus] =
await Promise.all([
getCheckoutOrder(orderId),
getCountries(),
getAddresses(),
checkAuth(),
]);
const [orderData, market, addressesData, authStatus] = await Promise.all([
getCheckoutOrder(orderId),
resolveMarket(urlCountry).catch(() => null),
getAddresses(),
checkAuth(),
]);

// Fetch countries scoped to the resolved market
const countriesData = market
? await getMarketCountries(market.id).catch(() => ({
data: [] as StoreCountry[],
}))
: { data: [] as StoreCountry[] };

Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!orderData) {
setError("Order not found or you don't have access to it.");
Expand Down Expand Up @@ -233,7 +240,7 @@ export default function CheckoutPage({ params }: CheckoutPageProps) {
} finally {
setLoading(false);
}
}, [orderId, basePath, router]);
}, [orderId, urlCountry, basePath, router]);

useEffect(() => {
loadOrder();
Expand Down
24 changes: 20 additions & 4 deletions src/components/layout/CountrySwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import type { StoreCountry } from "@spree/sdk";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { CheckIcon, ChevronDownIcon } from "@/components/icons";
import { useStore } from "@/contexts/StoreContext";
import { useCart } from "@/contexts/CartContext";
import { type CountryWithMarket, useStore } from "@/contexts/StoreContext";
import { updateOrderMarket } from "@/lib/data/checkout";
import { setStoreCookies } from "@/lib/utils/cookies";
import { getPathWithoutPrefix } from "@/lib/utils/path";

Expand All @@ -22,6 +23,7 @@ function countryToFlag(countryCode: string): string {

export function CountrySwitcher() {
const { country, currency, countries, setCountry, loading } = useStore();
const { cart, refreshCart } = useCart();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const router = useRouter();
Expand All @@ -42,12 +44,26 @@ export function CountrySwitcher() {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);

// Handle country selection — derive locale and currency from the country directly
const handleCountrySelect = (entry: StoreCountry) => {
// Handle country selection — derive locale and currency from the country's market
const handleCountrySelect = async (entry: CountryWithMarket) => {
const newLocale = entry.default_locale || "en";
const newCurrency = entry.currency;
const pathRest = getPathWithoutPrefix(pathname);
const newPath = `/${entry.iso.toLowerCase()}/${newLocale}${pathRest}`;

// Update existing cart if currency or locale changed
if (cart && (cart.currency !== newCurrency || cart.locale !== newLocale)) {
const result = await updateOrderMarket(cart.id, {
currency: newCurrency,
locale: newLocale,
});
if (!result.success) {
setIsOpen(false);
return;
}
await refreshCart();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

setStoreCookies(entry.iso.toLowerCase(), newLocale);
setCountry(entry.iso.toLowerCase());

Expand Down
63 changes: 47 additions & 16 deletions src/contexts/StoreContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import type { StoreCountry, StoreStore } from "@spree/sdk";
import type { StoreCountry, StoreMarket, StoreStore } from "@spree/sdk";
import { usePathname, useRouter } from "next/navigation";
import {
createContext,
Expand All @@ -12,17 +12,24 @@ import {
useRef,
useState,
} from "react";
import { getCountries as getCountriesAction } from "@/lib/data/countries";
import { getMarkets as getMarketsAction } from "@/lib/data/markets";
import { getStore as getStoreAction } from "@/lib/data/store";
import { setStoreCookies } from "@/lib/utils/cookies";
import { getPathWithoutPrefix } from "@/lib/utils/path";

/** Country enriched with market info (currency, locale, etc.) */
export interface CountryWithMarket extends StoreCountry {
currency: string;
default_locale: string;
marketId: string | null;
}

interface StoreContextValue {
country: string;
locale: string;
currency: string;
store: StoreStore | null;
countries: StoreCountry[];
countries: CountryWithMarket[];
setCountry: (country: string) => void;
setLocale: (locale: string) => void;
loading: boolean;
Expand All @@ -36,22 +43,45 @@ interface StoreProviderProps {
initialLocale: string;
}

/** Build a flat country list from markets, enriching each country with market info. */
function buildCountriesFromMarkets(
markets: StoreMarket[],
): CountryWithMarket[] {
const seen = new Set<string>();
const result: CountryWithMarket[] = [];

for (const market of markets) {
for (const country of market.countries ?? []) {
if (seen.has(country.iso)) continue;
seen.add(country.iso);

result.push({
...country,
currency: market.currency,
default_locale: market.default_locale,
marketId: market.id,
});
}
}

return result;
}

/** Find a country by ISO code in the flat countries list. */
function findCountry(
countries: StoreCountry[],
countries: CountryWithMarket[],
countryIso: string,
): StoreCountry | undefined {
): CountryWithMarket | undefined {
return countries.find(
(c) => c.iso.toLowerCase() === countryIso.toLowerCase(),
);
}

function resolveCountryAndCurrency(
countries: StoreCountry[],
storeData: StoreStore,
countries: CountryWithMarket[],
urlCountry: string,
): {
country: StoreCountry | undefined;
country: CountryWithMarket | undefined;
currency: string;
locale: string;
needsRedirect: boolean;
Expand Down Expand Up @@ -97,26 +127,26 @@ export function StoreProvider({
const [locale, setLocaleState] = useState(initialLocale);
const [currency, setCurrency] = useState("USD");
const [store, setStore] = useState<StoreStore | null>(null);
const [countries, setCountries] = useState<StoreCountry[]>([]);
const [countries, setCountries] = useState<CountryWithMarket[]>([]);
const [loading, setLoading] = useState(true);
const pathnameRef = useRef(pathname);
pathnameRef.current = pathname;

// Fetch store and countries data on mount
// Fetch store and markets data on mount
useEffect(() => {
const fetchData = async () => {
try {
const [storeData, countriesData] = await Promise.all([
const [storeData, marketsData] = await Promise.all([
getStoreAction(),
getCountriesAction(),
getMarketsAction(),
]);

setStore(storeData);
setCountries(countriesData.data);
const enrichedCountries = buildCountriesFromMarkets(marketsData.data);
setCountries(enrichedCountries);

const resolved = resolveCountryAndCurrency(
countriesData.data,
storeData,
enrichedCountries,
initialCountry,
);

Expand Down Expand Up @@ -153,8 +183,9 @@ export function StoreProvider({
(newCountry: string): void => {
setCountryState(newCountry);
const countryObj = findCountry(countries, newCountry);
if (countryObj?.currency) {
if (countryObj) {
setCurrency(countryObj.currency);
setLocaleState(countryObj.default_locale);
}
},
[countries],
Expand Down
Loading