ux: add country document json info as static asset (#1670)

* add country document json info as static asset

* add staleness test

* update test

* formatting
This commit is contained in:
Justin Hernandez
2026-01-30 10:25:51 -08:00
committed by GitHub
parent a6c84d80f7
commit a96777d80a
3 changed files with 361 additions and 31 deletions

View File

@@ -0,0 +1,256 @@
{
"ABW": ["p", "i"],
"AFG": ["p"],
"AGO": ["p", "i"],
"AIA": ["p", "i"],
"ALA": ["p", "i"],
"ALB": ["p", "i"],
"AND": ["p", "i"],
"ARE": ["p", "i"],
"ARG": ["p", "i"],
"ARM": ["p", "i"],
"ASM": ["p", "i"],
"ATA": ["p", "i"],
"ATF": ["p", "i"],
"ATG": ["p", "i"],
"AUS": ["p", "i"],
"AUT": ["p", "i"],
"AZE": ["p", "i"],
"BDI": ["p", "i"],
"BEL": ["p", "i"],
"BEN": ["p", "i"],
"BES": ["p", "i"],
"BFA": ["p", "i"],
"BGD": ["p", "i"],
"BGR": ["p", "i"],
"BHR": ["p", "i"],
"BHS": ["p", "i"],
"BIH": ["p", "i"],
"BLM": ["p", "i"],
"BLR": ["p", "i"],
"BLZ": ["p", "i"],
"BMU": ["p", "i"],
"BOL": ["p", "i"],
"BRA": ["p", "i"],
"BRB": ["p", "i"],
"BRN": ["p", "i"],
"BTN": ["p", "i"],
"BVT": ["p", "i"],
"BWA": ["p", "i"],
"CAF": ["p", "i"],
"CAN": ["p", "i"],
"CCK": ["p", "i"],
"CHE": ["p", "i"],
"CHL": ["p", "i"],
"CHN": ["p", "i"],
"CIV": ["p", "i"],
"CMR": ["p", "i"],
"COD": ["p", "i"],
"COG": ["p", "i"],
"COK": ["p", "i"],
"COL": ["p", "i"],
"COM": ["p", "i"],
"CPV": ["p", "i"],
"CRI": ["p", "i"],
"CUB": ["p", "i"],
"CUW": ["p", "i"],
"CXR": ["p", "i"],
"CYM": ["p", "i"],
"CYP": ["p", "i"],
"CZE": ["p", "i"],
"D<<": ["p", "i"],
"DJI": ["p", "i"],
"DMA": ["p", "i"],
"DNK": ["p", "i"],
"DOM": ["p", "i"],
"DZA": ["p", "i"],
"ECU": ["p", "i"],
"EGY": [],
"ERI": ["p", "i"],
"ESH": ["p", "i"],
"ESP": ["p", "i"],
"EST": ["p", "i"],
"ETH": ["p", "i"],
"EUE": ["p", "i"],
"FIN": ["p", "i"],
"FJI": ["p", "i"],
"FLK": ["p", "i"],
"FRA": ["p", "i"],
"FRO": ["p", "i"],
"FSM": ["p", "i"],
"GAB": ["p", "i"],
"GBR": ["p", "i"],
"GEO": ["p", "i"],
"GGY": ["p", "i"],
"GHA": ["p", "i"],
"GIB": ["p", "i"],
"GIN": ["p", "i"],
"GLP": ["p", "i"],
"GMB": ["p", "i"],
"GNB": ["p", "i"],
"GNQ": ["p", "i"],
"GRC": ["p", "i"],
"GRD": ["p", "i"],
"GRL": ["p", "i"],
"GTM": ["p", "i"],
"GUF": ["p", "i"],
"GUM": ["p", "i"],
"GUY": ["p", "i"],
"HKG": ["p", "i"],
"HMD": ["p", "i"],
"HND": ["p", "i"],
"HRV": ["p", "i"],
"HTI": ["p", "i"],
"HUN": ["p", "i"],
"IDN": ["p", "i"],
"IMN": ["p", "i"],
"IND": ["p", "a"],
"IOT": ["p", "i"],
"IRL": ["p", "i"],
"IRN": ["p", "i"],
"IRQ": ["p", "i"],
"ISL": ["p", "i"],
"ISR": ["p", "i"],
"ITA": ["p", "i"],
"JAM": ["p", "i"],
"JEY": ["p", "i"],
"JOR": ["p", "i"],
"JPN": ["p", "i"],
"KAZ": ["p", "i"],
"KEN": ["p", "i"],
"KGZ": ["p", "i"],
"KHM": ["p", "i"],
"KIR": ["p", "i"],
"KNA": ["p", "i"],
"KOR": ["p", "i"],
"KWT": ["p", "i"],
"LAO": ["p", "i"],
"LBN": ["p", "i"],
"LBR": ["p", "i"],
"LBY": ["p", "i"],
"LCA": ["p", "i"],
"LIE": ["p", "i"],
"LKA": ["p", "i"],
"LSO": ["p", "i"],
"LTU": ["p", "i"],
"LUX": ["p", "i"],
"LVA": ["p", "i"],
"MAC": ["p", "i"],
"MAF": ["p", "i"],
"MAR": ["p", "i"],
"MCO": ["p", "i"],
"MDA": ["p", "i"],
"MDG": ["p", "i"],
"MDV": ["p", "i"],
"MEX": ["p", "i"],
"MHL": ["p", "i"],
"MKD": ["p", "i"],
"MLI": ["p", "i"],
"MLT": ["p", "i"],
"MMR": ["p", "i"],
"MNE": ["p", "i"],
"MNG": ["p", "i"],
"MNP": ["p", "i"],
"MOZ": ["p", "i"],
"MRT": ["p", "i"],
"MSR": ["p", "i"],
"MTQ": ["p", "i"],
"MUS": ["p", "i"],
"MWI": ["p", "i"],
"MYS": ["p", "i"],
"MYT": ["p", "i"],
"NAM": ["p", "i"],
"NCL": ["p", "i"],
"NER": ["p", "i"],
"NFK": ["p", "i"],
"NGA": ["p", "i"],
"NIC": ["p", "i"],
"NIU": ["p", "i"],
"NLD": ["p", "i"],
"NOR": ["p", "i"],
"NPL": ["p", "i"],
"NRU": ["p", "i"],
"NZL": ["p", "i"],
"OMN": ["p", "i"],
"PAK": ["p", "i"],
"PAN": ["p", "i"],
"PCN": ["p", "i"],
"PER": ["p", "i"],
"PHL": ["p", "i"],
"PLW": ["p", "i"],
"PNG": ["p", "i"],
"POL": ["p", "i"],
"PRI": ["p", "i"],
"PRK": ["p", "i"],
"PRT": ["p", "i"],
"PRY": ["p", "i"],
"PSE": ["p", "i"],
"PYF": ["p", "i"],
"QAT": ["p", "i"],
"REU": ["p", "i"],
"ROU": ["p", "i"],
"RUS": ["p", "i"],
"RWA": ["p", "i"],
"SAU": ["p", "i"],
"SDN": ["p", "i"],
"SEN": ["p", "i"],
"SGP": ["p", "i"],
"SGS": ["p", "i"],
"SHN": ["p", "i"],
"SJM": ["p", "i"],
"SLB": ["p", "i"],
"SLE": ["p", "i"],
"SLV": ["p", "i"],
"SMR": ["p", "i"],
"SOM": ["p", "i"],
"SPM": ["p", "i"],
"SRB": ["p", "i"],
"SSD": ["p", "i"],
"STP": ["p", "i"],
"SUR": ["p", "i"],
"SVK": ["p", "i"],
"SVN": ["p", "i"],
"SWE": ["p", "i"],
"SWZ": ["p", "i"],
"SXM": ["p", "i"],
"SYC": ["p", "i"],
"SYR": ["p", "i"],
"TCA": ["p", "i"],
"TCD": ["p", "i"],
"TGO": ["p", "i"],
"THA": ["p", "i"],
"TJK": ["p", "i"],
"TKL": ["p", "i"],
"TKM": ["p", "i"],
"TLS": ["p", "i"],
"TON": ["p", "i"],
"TTO": ["p", "i"],
"TUN": ["p", "i"],
"TUR": ["p", "i"],
"TUV": ["p", "i"],
"TWN": ["p", "i"],
"TZA": ["p", "i"],
"UGA": ["p", "i"],
"UKR": ["p", "i"],
"UMI": ["p", "i"],
"UNO": ["p", "i"],
"URY": ["p", "i"],
"USA": ["p", "i"],
"UZB": ["p", "i"],
"VAT": ["p", "i"],
"VCT": ["p", "i"],
"VEN": ["p", "i"],
"VGB": ["p", "i"],
"VIR": ["p", "i"],
"VNM": ["p", "i"],
"VUT": ["p", "i"],
"WLF": ["p", "i"],
"WSM": ["p", "i"],
"XCE": ["p", "i"],
"XOM": ["p", "i"],
"XPO": ["p", "i"],
"YEM": ["p", "i"],
"ZAF": ["p", "i"],
"ZMB": ["p", "i"],
"ZWE": ["p", "i"]
}

View File

@@ -2,12 +2,14 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { getCountry } from 'react-native-localize';
import { commonNames } from '@selfxyz/common';
import { alpha2ToAlpha3 } from '@selfxyz/common/constants/countries';
import countryDocumentTypesData from '../data/country-document-types.json';
export interface CountryData {
[countryCode: string]: string[];
}
@@ -29,38 +31,11 @@ function getUserCountryCode(): string | null {
}
return null;
}
export function useCountries() {
const [countryData, setCountryData] = useState<CountryData>({});
const [loading, setLoading] = useState(true);
const countryData = countryDocumentTypesData as CountryData;
const userCountryCode = useMemo(getUserCountryCode, []);
useEffect(() => {
const controller = new AbortController();
const fetchCountryData = async () => {
try {
const response = await fetch('https://api.staging.self.xyz/id-picker', {
signal: controller.signal,
});
const result = await response.json();
if (result.status === 'success') {
setCountryData(result.data);
// if (__DEV__) {
// console.log('Set country data:', result.data);
// }
} else {
console.error('API returned non-success status:', result.status);
}
} catch (error) {
console.error('Error fetching country data:', error);
} finally {
setLoading(false);
}
};
fetchCountryData();
return () => controller.abort();
}, []);
const countryList = useMemo(() => {
const allCountries = Object.keys(countryData).map(countryCode => ({
key: countryCode,
@@ -77,5 +52,5 @@ export function useCountries() {
const showSuggestion = userCountryCode && countryData[userCountryCode];
return { countryData, countryList, loading, userCountryCode, showSuggestion };
return { countryData, countryList, loading: false, userCountryCode, showSuggestion };
}

View File

@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* Integration test for country data synchronization.
*
* This test verifies that the bundled country-document-types.json matches
* the staging API response. It gracefully skips when network is unavailable
* to avoid CI flakiness from transient network issues.
*
* To run integration tests only: yarn test --grep="integration"
* To skip integration tests: yarn test --grep="^(?!.*integration)"
*/
import { describe, expect, it } from 'vitest';
import countryDocumentTypesData from '../../src/data/country-document-types.json';
/**
* Helper to check if an error is a network-related error that should cause
* the test to skip rather than fail.
*/
function isNetworkError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const networkErrorPatterns = [
'ENOTFOUND', // DNS resolution failed
'ECONNREFUSED', // Connection refused
'ECONNRESET', // Connection reset
'ETIMEDOUT', // Connection timed out
'EAI_AGAIN', // DNS temporary failure
'ENETUNREACH', // Network unreachable
'EHOSTUNREACH', // Host unreachable
'fetch failed', // Generic fetch failure
'network', // Generic network error
'AbortError', // Request aborted (timeout)
];
const errorMessage = error.message.toLowerCase();
const errorName = error.name;
return networkErrorPatterns.some(
pattern =>
errorMessage.includes(pattern.toLowerCase()) ||
errorName === pattern ||
('cause' in error &&
error.cause instanceof Error &&
error.cause.message.toLowerCase().includes(pattern.toLowerCase())),
);
}
describe('Country data synchronization [integration]', () => {
it('bundled data should match API response', async ({ skip }) => {
// Fetch current data from staging API with timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
let response: Response;
try {
response = await fetch('https://api.staging.self.xyz/id-picker', {
signal: controller.signal,
});
} catch (error) {
// Network errors should skip the test, not fail it
if (isNetworkError(error)) {
skip();
return;
}
throw error;
} finally {
clearTimeout(timeoutId);
}
// Non-2xx responses that aren't network errors should also skip
// (e.g., 503 Service Unavailable, 502 Bad Gateway)
if (!response.ok) {
if (response.status >= 500) {
skip();
return;
}
// 4xx errors are likely real issues, so we let them fail
expect.fail(`API returned ${response.status}: ${response.statusText}`);
}
const result = await response.json();
expect(result.status).toBe('success');
const apiData = result.data;
const bundledData = countryDocumentTypesData;
// Compare the data structures
expect(bundledData).toEqual(apiData);
// If this test fails, it means the API has been updated with new countries
// or document types that aren't in the bundled data yet.
// To fix: Update src/data/country-document-types.json with the latest API data.
}, 10000); // 10s Vitest timeout
});