- Регистрация
- 1 Мар 2015
- Сообщения
- 6,535
- Баллы
- 155
Prerequisites
Before you begin, ensure you have abasic understanding of Next.js and React.
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>(
`{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 . Visit the API Keys Page: Navigate to the page.
Visit:
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 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;
};
Before you begin, ensure you have abasic understanding of Next.js and React.
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>(
`{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 . Visit the API Keys Page: Navigate to the page.
Visit:
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 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;
};