Blogs

Intellibot Creation Blog.

Building an Automatic Currency Switcher in Next.js

Discover how to implement an automatic currency switcher in Next.js to offer dynamic pricing based on user location or currency selection.  Learn to build a currency switcher for your Next.js app, allowing users to view prices in their preferred currency in real time.  Create an automatic currency switcher in Next.js, enhancing the user experience by providing location-based or selectable pricing options.  Build a seamless, real-time currency conversion feature for your Next.js application to cater to an international audience.  Master the process of setting up an automatic currency switcher in Next.js, perfect for global websites and e-commerce platforms.
Authored by Shahrukh Javed, this guide offers a step-by-step approach to building an automatic currency switcher in Next.js, designed to enhance the user experience with dynamic pricing.
Muhammad shahrukh

1. Creating the Backend API Route

We'll create a Next.js API route that interacts with our Geolocation API.
Create a new file at: src/app/api/geolocation/route.ts

import { NextResponse } from "next/server";
import axios from "axios";

type IPGeolocation = {
ip: string;
version?: string;
city?: string;
region?: string;
region_code?: string;
country_code?: string;
country_code_iso3?: string;
country_fifa_code?: string;
country_fips_code?: string;
country_name?: string;
country_capital?: string;
country_tld?: string;
country_emoji?: string;
continent_code?: string;
in_eu: boolean;
land_locked: boolean;
postal?: string;
latitude?: number;
longitude?: number;
timezone?: string;
utc_offset?: string;
country_calling_code?: string;
currency?: string;
currency_name?: string;
languages?: string;
country_area?: number;
asn?: string; // Append ?fields=asn to the URL
isp?: string; // Append ?fields=isp to the URL
}

type IPGeolocationError = {
code: string;
error: string;
}

export async function GET() {
// Retrieve IP address using the getClientIp function
// For testing purposes, we'll use a fixed IP address
// const clientIp = getClientIp(req.headers);

const clientIp = "84.17.50.173";

if (!clientIp) {
return NextResponse.json(
{ error: "Unable to determine IP address" },
{ status: 400 }
);
}

const key = process.env.IPFLARE_API_KEY;

if (!key) {
return NextResponse.json(
{ error: "IPFlare API key is not set" },
{ status: 500 }
);
}

try {
const response = await axios.get<IPGeolocation | IPGeolocationError>(
`https://api.ipflare.io/${clientIp}`,
{
headers: {
"X-API-Key": key,
},
}
);

if ("error" in response.data) {
return NextResponse.json({ error: response.data.error }, { status: 400 });
}

return NextResponse.json(response.data);
} catch {
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
);
}
}

2. Obtaining Your API Key

We are going to use a free geolocation service called IP Flare. Visit the API Keys Page: Navigate to the API Keys page.

Visit: www.ipflare.io

From the API Keys page we can get our API key and we can use the quick copy to store it as an environment variable in our .env file. We will use this to authenticate our requests.

3. Creating the Frontend Component

I have created this all-in-one component that includes the provider and the currency selector. I am using shadcn/ui and some flag SVGs I found online.

You will need to wrap the application in the <CurrencyProvider /> so that we can access the context.

Now, anywhere in the application where we want to access the currency, we can use the hook const { currency } = useCurrency();.

To integrate this with Stripe, when you create the checkout you just need to send the currency and ensure that you have added multi-currency pricing to your Stripe products.

"use client";

import { useRouter } from "next/navigation";
import {
createContext,
type FC,
type ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import axios from "axios"; // 1) Import axios
import { Flag } from "~/components/flag";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { cn } from "~/lib/utils";
import { type Currency } from "~/server/schemas/currency";

// -- [1] Create a local type for the data returned by /api/geolocation.
type GeolocationData = {
country_code?: string;
continent_code?: string;
currency?: string;
};

type CurrencyContext = {
currency: Currency;
setCurrency: (currency: Currency) => void;
};

const CurrencyContext = createContext<CurrencyContext | null>(null);

export function useCurrency() {
const context = useContext(CurrencyContext);
if (!context) {
throw new Error("useCurrency must be used within a CurrencyProvider.");
}
return context;
}

export const CurrencyProvider: FC<{ children: ReactNode }> = ({ children }) => {
const router = useRouter();

// -- [2] Local state for geolocation data
const [location, setLocation] = useState<GeolocationData | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);

// -- [3] Fetch location once when the component mounts
useEffect(() => {
const fetchLocation = async () => {
setIsLoading(true);
try {
const response = await axios.get("/api/geolocation");
setLocation(response.data);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};

void fetchLocation();
}, []);

// -- [4] Extract currency from location if present (fallback to "usd")
const geoCurrency = location?.currency;

const getInitialCurrency = (): Currency => {
if (typeof window !== "undefined") {
const cookie = document.cookie
.split("; ")
.find((row) => row.startsWith("currency="));
if (cookie) {
const value = cookie.split("=")[1];
if (value === "usd" || value === "eur" || value === "gbp") {
return value;
}
}
}
return "usd";
};

const [currency, setCurrencyState] = useState<Currency>(getInitialCurrency);

useEffect(() => {
if (!isLoading && geoCurrency !== undefined) {
const validatedCurrency = validateCurrency(geoCurrency, location);
if (validatedCurrency) {
setCurrency(validatedCurrency);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading, location, geoCurrency]);

// -- [5] Update currency & store cookie; no more tRPC invalidation
const setCurrency = (newCurrency: Currency) => {
setCurrencyState(newCurrency);
if (typeof window !== "undefined") {
document.cookie = `currency=${newCurrency}; path=/; max-age=${
60 * 60 * 24 * 365
}`; // Expires in 1 year
}
// Removed tRPC invalidate since we are no longer using tRPC
router.refresh();
};

const contextValue = useMemo<CurrencyContext>(
() => ({
currency,
setCurrency,
}),
[currency],
);

return (
<CurrencyContext.Provider value={contextValue}>
{children}
</CurrencyContext.Provider>
);
};

export const CurrencySelect = ({ className }: { className?: string }) => {
const { currency, setCurrency } = useCurrency();
return (
<Select value={currency} onValueChange={setCurrency}>
<SelectTrigger className={cn("w-[250px]", className)}>
<SelectValue placeholder="Select a currency" />
</SelectTrigger>
<SelectContent>
<SelectGroup className="text-sm">
<SelectItem value="usd">
<div className="flex items-center gap-3">
<Flag code="US" className="h-4 w-4 rounded" /> <span>$ USD</span>
</div>
</SelectItem>
<SelectItem value="eur">
<div className="flex items-center gap-3">
<Flag code="EU" className="h-4 w-4 rounded" /> <span>€ EUR</span>
</div>
</SelectItem>
<SelectItem value="gbp">
<div className="flex items-center gap-3">
<Flag code="GB" className="h-4 w-4 rounded" /> <span>£ GBP</span>
</div>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
);
};

// -- [6] Use our new GeolocationData type in place of RouterOutputs
const validateCurrency = (
currency: string,
location?: GeolocationData | null,
): Currency | null => {
if (currency === "usd" || currency === "eur" || currency === "gbp") {
return currency;
}

if (!location) {
return null;
}

if (location.country_code === "GB") {
return "gbp";
}

// Check if they are in the EU
if (location.continent_code === "EU") {
return "eur";
}

// North America
if (location.continent_code === "NA") {
return "usd";
}

return null;
};

Comment Section

No comments yet.