在 Next.js 中构建自动货币转换器
**先决条件**
在开始之前,请确保您对 Next.js 和 React 有基本的了解。
1.创建后端 API 路由
我们将创建一个与我们的地理位置 API 交互的 Next.js API 路由。
在以下位置创建一个新文件:`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(
`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.获取您的 API 密钥
我们将使用名为 IP Flare 的 **免费** 地理定位服务。访问 API 密钥页面:导航到 API 密钥页面。
访问:**www.ipflare.io**
从 API 密钥页面我们可以获取 API 密钥,并且可以使用快速复制将其作为环境变量存储在我们的 `.env` 文件中。我们将使用它来验证我们的请求。

3. 创建前端组件
我创建了这个包含提供商和货币选择器的一体化组件。我使用的是 shadcn/ui 和一些我在网上找到的旗帜 SVG。
您需要将应用程序包装在`` 以便我们能够访问上下文。
现在,在应用程序中任何我们想要访问货币的地方,我们都可以使用钩子 `const { currency } = useCurrency();`。
要将其与 Stripe 集成,当您创建结帐时,您只需发送货币并确保已为 Stripe 产品添加了多币种定价。
"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(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(null);
const [isLoading, setIsLoading] = useState(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(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(
() => ({
currency,
setCurrency,
}),
[currency],
);
return (
{children}
);
};
export const CurrencySelect = ({ className }: { className?: string }) => {
const { currency, setCurrency } = useCurrency();
return (
);
};
// -- [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;
};