Skip to content
Closed
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
10 changes: 6 additions & 4 deletions src/__tests__/PolicyEngine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,25 @@ afterEach(() => {
});

describe("PolicyEngine.jsx", () => {
test("Renders for US if proper data passed", () => {
test("Renders for US if proper data passed", async () => {
useSearchParams.mockImplementation(() => [
new URLSearchParams(),
jest.fn(),
]);

const { getByText } = render(
const { findByText } = render(
<BrowserRouter>
<PolicyEngine />
</BrowserRouter>,
);

expect(
getByText("Computing Public Policy for Everyone"),
await findByText("Computing Public Policy for Everyone"),
).toBeInTheDocument();
expect(getByText("Trusted across the US")).toBeInTheDocument();

expect(await findByText("Trusted across the US")).toBeInTheDocument();
});

test("Converts deprecated 'region=enhanced_us' URL search param to 'region=us' and 'dataset=enhanced_cps'", () => {
const deprecatedEnhancedUsParams = new URLSearchParams({
region: "enhanced_us",
Expand Down
32 changes: 13 additions & 19 deletions src/posts/articles/farage-speech-may-2025.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
" \"gov.hmrc.income_tax.rates.uk[1].threshold\": 50_000,\n",
" \"gov.hmrc.income_tax.allowances.marriage_allowance.max\": 0.25,\n",
" \"gov.dwp.universal_credit.elements.child.limit.child_count\": 90,\n",
" },\n",
" }\n",
")"
]
},
Expand Down Expand Up @@ -163,7 +163,7 @@
"fig = px.bar(\n",
" y=result.decile.relative,\n",
" text=[f\"{x:.1%}\" for x in result.decile.relative.values()],\n",
" color_discrete_sequence=[BLUE],\n",
" color_discrete_sequence=[BLUE]\n",
").update_layout(\n",
" title=\"Impact of Reform UK tax-benefit policies by income decile\",\n",
" xaxis_title=\"Income decile\",\n",
Expand All @@ -172,7 +172,8 @@
" yaxis_tickformat=\".0%\",\n",
")\n",
"\n",
"print(format_fig(fig).update_layout().to_json())"
"print(format_fig(fig).update_layout(\n",
").to_json())"
]
},
{
Expand Down Expand Up @@ -322,14 +323,11 @@
" print(reform, policy_reform_names[i])\n",
"\n",
" for year in tqdm(range(2025, 2030)):\n",
" debt_impact = (\n",
" sim.reform_simulation.calculate(\"gov_balance\", year).sum() / 1e9\n",
" - sim.baseline_simulation.calculate(\"gov_balance\", year).sum()\n",
" / 1e9\n",
" )\n",
" debt_impact = sim.reform_simulation.calculate(\"gov_balance\", year).sum()/1e9 - sim.baseline_simulation.calculate(\"gov_balance\", year).sum()/1e9\n",
" years.append(year)\n",
" debt_impacts.append(debt_impact)\n",
" policies.append(policy_reform_names[i])"
" policies.append(policy_reform_names[i])\n",
"\n"
]
},
{
Expand All @@ -341,17 +339,13 @@
"source": [
"import pandas as pd\n",
"\n",
"df = pd.DataFrame(\n",
" {\n",
" \"Year\": years,\n",
" \"Debt impact (£ billions)\": debt_impacts,\n",
" \"Policy\": policies,\n",
" }\n",
")\n",
"df = pd.DataFrame({\n",
" \"Year\": years,\n",
" \"Debt impact (£ billions)\": debt_impacts,\n",
" \"Policy\": policies,\n",
"})\n",
"\n",
"df = df.pivot(\n",
" columns=\"Year\", index=\"Policy\", values=\"Debt impact (£ billions)\"\n",
")\n",
"df = df.pivot(columns=\"Year\", index=\"Policy\", values=\"Debt impact (£ billions)\")\n",
"df = df.iloc[[2, 1, 3, 0]]"
]
},
Expand Down
42 changes: 19 additions & 23 deletions src/routing/RedirectToCountry.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
import { useState, useEffect } from "react";
import { Navigate } from "react-router-dom";
import { getCountryId } from "../utils/ipinfoCountry.js";

/**
* Redirects the user to their country-specific route based on their IP address.
*/
export default function RedirectToCountry() {
// Find country ID
const countryId = findCountryId();
const [countryId, setCountryId] = useState(null);

return <Navigate to={`/${countryId}`} replace />;
}
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;

/**
* Based on the URL and user's browser, determine country ID;
* if not possible, return "us" as country ID
* @returns {String}
*/
export function findCountryId() {
const COUNTRY_CODES = {
"en-US": "us",
"en-GB": "uk",
"en-CA": "ca",
"en-NG": "ng",
"en-IL": "il",
};
const fetchCountry = async () => {
const id = await getCountryId(signal);
setCountryId(id);
};

const browserLanguage = navigator.language;
fetchCountry();
return () => controller.abort();
}, []);

if (Object.keys(COUNTRY_CODES).includes(browserLanguage)) {
return COUNTRY_CODES[browserLanguage];
} else {
return "us";
}
if (countryId === null) return null;

return <Navigate to={`/${countryId}`} replace />;
}
88 changes: 88 additions & 0 deletions src/utils/ipinfoCountry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @file utils/ipinfoCountry.js
*
* Determines the PolicyEngine country segment ("uk", "us", …) for the user
* via:
*
* IP geolocation or Browser language and defaults to "us"
*
* @typedef {"uk" | "us" | "ca" | "ng" | "il"} CountryId
*/

/**
* Maps ISO codes to country-id.
*/
const ISO2_TO_SEGMENT = {
gb: "uk",
im: "uk", // Isle of Man
je: "uk", // Jersey
gg: "uk", // Guernsey
us: "us",
ca: "ca",
ng: "ng",
il: "il",
};

/**
* Maps navigator.language strings to country-id.
*/
const LANGUAGE_TO_SEGMENT = {
"en-GB": "uk",
"en-US": "us",
"en-CA": "ca",
"en-NG": "ng",
"en-IL": "il",
};

/**
* Attempt to get a 'CountryId' from the client's public IP via ipinfo.io.
*
* @param {AbortSignal} [signal]
* @returns {Promise<CountryId | null>}
*/
export async function resolveCountryFromIp(signal) {
const token = process.env.REACT_APP_IPINFO_TOKEN;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue, blocking: Please reopen this PR off of a branch, not a fork

As a fork contributor, I believe you don't have access to this environment variable, and I'm sorry that I forgot to advise you on this. Could you reopen this PR off of a branch? Let me know if you need additional permissions to do so.

if (!token) {
console.warn("Missing ipinfo token – skipping IP lookup");
return null;
}

const endpoint = `https://api.ipinfo.io/lite/me?token=${token}`;
const resp = await fetch(endpoint, { signal });
if (!resp.ok) throw new Error(`ipinfo returned ${resp.status}`);

const { country_code: iso2 = "" } = await resp.json();
return ISO2_TO_SEGMENT[iso2.toLowerCase()] ?? null;
}

/**
* Get country-id from browser language.
*
* @returns {CountryId | null}
*/
export function resolveCountryFromLanguage() {
return LANGUAGE_TO_SEGMENT[navigator.language] ?? null;
}

/**
* Get country id by IP or browser language and default to "us".
*
* @param {AbortSignal} [signal]
* @returns {Promise<CountryId>}
*/
export async function getCountryId(signal) {
try {
const countryCode_Ip = await resolveCountryFromIp(signal);
if (countryCode_Ip) return countryCode_Ip;
} catch (err) {
if (signal?.aborted) throw err;
console.error("IP‑based country lookup failed:", err);
}

// Using browser language
const browserLang = resolveCountryFromLanguage();
if (browserLang) return browserLang;

//default
return "us";
}
Loading