diff --git a/.env.example b/.env.example index db42dfa2..92c38b9e 100644 --- a/.env.example +++ b/.env.example @@ -95,6 +95,45 @@ BREVO_LIST_ID= # Get from: Brevo Dashboard → Conversations → Settings NEXT_PUBLIC_BREVO_CONVERSATIONS_ID= NEXT_PUBLIC_BREVO_CONVERSATIONS_GROUP_ID= +# ============================================================================= +# Phone Verification Services +# ============================================================================= + +# KudiSMS (for African phone numbers) +# Get from: KudiSMS Dashboard → Settings → API Keys +KUDISMS_API_KEY=your_kudisms_api_key +KUDISMS_APP_NAME_CODE=your_app_name_code +KUDISMS_TEMPLATE_CODE=your_template_code +KUDISMS_SENDER_ID=Noblocks + +# Twilio (for international phone numbers via Verify API) +# Get from: Twilio Console → Account Dashboard +TWILIO_ACCOUNT_SID=your_twilio_account_sid +TWILIO_AUTH_TOKEN=your_twilio_auth_token +# Verify service SID: Twilio Console → Verify → Services → create or use default +TWILIO_VERIFY_SERVICE_SID=VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# ============================================================================= +# SmileID KYC Verification Services +# ============================================================================= + +SMILE_IDENTITY_BASE_URL=XXXXXX #get from docs +SMILE_IDENTITY_API_KEY=your_api_key_here +SMILE_IDENTITY_PARTNER_ID=your_partner_id_here +SMILE_ID_CALLBACK_URL= #optional callback url +SMILE_IDENTITY_SERVER=0 # 0 for sandbox, 1 for production + +# ============================================================================= +# Dojah – Tier 3 address / proof-of-address verification +# ============================================================================= +# Get from: https://app.dojah.io/developers/configuration +DOJAH_APP_ID=6977612b48b1f4961adee48c +DOJAH_SECRET_KEY=test_sk_S71SCe2U5LRXFcUuyqXtWCEse +# Optional: use https://sandbox.dojah.io for testing +DOJAH_BASE_URL=https://sandbox.dojah.io +# Supabase Storage bucket for KYC document uploads (create in Supabase Dashboard → Storage) +KYC_DOCUMENTS_BUCKET=kyc-documents + # ============================================================================= # Campaign Management (BlockFest) # ============================================================================= diff --git a/app/api/aggregator.ts b/app/api/aggregator.ts index eb213242..f0b5cf42 100644 --- a/app/api/aggregator.ts +++ b/app/api/aggregator.ts @@ -7,6 +7,7 @@ import type { VerifyAccountPayload, InitiateKYCPayload, InitiateKYCResponse, + SmileIDSubmissionResponse, KYCStatusResponse, OrderDetailsResponse, TransactionResponse, @@ -771,4 +772,60 @@ export async function migrateLocalStorageRecipients( console.error("Error migrating recipients:", error); // Don't throw - let the app continue even if migration fails } -} +}; + +/** + * Submits Smile ID captured data for KYC verification + * @param {object} payload - The Smile ID data payload + * @param {string} accessToken - The access token for authentication + * @returns {Promise} The submission response + * @throws {Error} If the API request fails + */ +export const submitSmileIDData = async ( + payload: any, + accessToken: string, +): Promise => { + const startTime = Date.now(); + + try { + // Track external API request (log metadata only, no PII) + trackServerEvent("External API Request", { + service: "next-api", + endpoint: "/api/kyc/smile-id", + method: "POST", + }); + + // Call Next.js API route with JWT authentication + const response = await axios.post(`/api/kyc/smile-id`, payload, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + // Track successful response + const responseTime = Date.now() - startTime; + trackApiResponse("/api/kyc/smile-id", "POST", 200, responseTime, { + service: "next-api", + }); + + // Track business event + trackBusinessEvent("Smile ID Data Submitted", { + jobId: response.data.data?.jobId, + }); + + return response.data; + } catch (error) { + const responseTime = Date.now() - startTime; + + // Track API error + trackServerEvent("External API Error", { + service: "next-api", + endpoint: "/api/kyc/smile-id", + method: "POST", + error_message: error instanceof Error ? error.message : "Unknown error", + response_time_ms: responseTime, + }); + + throw error; + } +}; \ No newline at end of file diff --git a/app/api/kyc/smile-id/id_types.json b/app/api/kyc/smile-id/id_types.json new file mode 100644 index 00000000..22449a59 --- /dev/null +++ b/app/api/kyc/smile-id/id_types.json @@ -0,0 +1,2734 @@ +{ + "continents": [ + { + "name": "Africa", + "countries": [ + { + "name": "Algeria", + "code": "DZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Angola", + "code": "AO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Benin", + "code": "BJ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Botswana", + "code": "BW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Burkina Faso", + "code": "BF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Burundi", + "code": "BI", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cameroon", + "code": "CM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cape Verde", + "code": "CV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Central African Republic", + "code": "CF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Chad", + "code": "TD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Comoros", + "code": "KM", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Congo", + "code": "CG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Congo, Democratic Republic of the", + "code": "CD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cote d'Ivoire", + "code": "CI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Djibouti", + "code": "DJ", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Egypt", + "code": "EG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Equatorial Guinea", + "code": "GQ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Eritrea", + "code": "ER", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Eswatini", + "code": "SZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ethiopia", + "code": "ET", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Gabon", + "code": "GA", + "id_types": [ + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Gambia", + "code": "GM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ghana", + "code": "GH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guinea", + "code": "GN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guinea-Bissau", + "code": "GW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kenya", + "code": "KE", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "NATIONAL_ID", "verification_method": "doc_verification"}, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lesotho", + "code": "LS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Liberia", + "code": "LR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Libya", + "code": "LY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Madagascar", + "code": "MG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Malawi", + "code": "MW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mali", + "code": "ML", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mauritania", + "code": "MR", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mauritius", + "code": "MU", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Morocco", + "code": "MA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mozambique", + "code": "MZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Namibia", + "code": "NA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Niger", + "code": "NE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nigeria", + "code": "NG", + "id_types": [ + { "type": "BVN", "verification_method": "biometric_kyc" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "NIN_SLIP", "verification_method": "biometric_kyc" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "V_NIN", "verification_method": "biometric_kyc" } + ] + }, + { + "name": "Rwanda", + "code": "RW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Helena, Ascension and Tristan da Cunha", + "code": "SH", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sao Tome and Principe", + "code": "ST", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Senegal", + "code": "SN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Seychelles", + "code": "SC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sierra Leone", + "code": "SL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Somalia", + "code": "SO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "South Africa", + "code": "ZA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "South Sudan", + "code": "SS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sudan", + "code": "SD", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tanzania, United Republic of", + "code": "TZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Togo", + "code": "TG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tunisia", + "code": "TN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Uganda", + "code": "UG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Western Sahara", + "code": "EH", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Zambia", + "code": "ZM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Zimbabwe", + "code": "ZW", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "Asia and the Middle East", + "countries": [ + { + "name": "Afghanistan", + "code": "AF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Armenia", + "code": "AM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Azerbaijan", + "code": "AZ", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bahrain", + "code": "BH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bangladesh", + "code": "BD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bhutan", + "code": "BT", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Brunei Darussalam", + "code": "BN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cambodia", + "code": "KH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "China", + "code": "CN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cyprus", + "code": "CY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Georgia", + "code": "GE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Hong Kong", + "code": "HK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "India", + "code": "IN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Indonesia", + "code": "ID", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Iran, Islamic Republic of", + "code": "IR", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Iraq", + "code": "IQ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Israel", + "code": "IL", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Japan", + "code": "JP", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Jordan", + "code": "JO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kazakhstan", + "code": "KZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Korea, Democratic People's Republic of", + "code": "KP", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Korea, Republic of", + "code": "KR", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kuwait", + "code": "KW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kyrgyzstan", + "code": "KG", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lao People's Democratic Republic", + "code": "LA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lebanon", + "code": "LB", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Macao", + "code": "MO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Malaysia", + "code": "MY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Maldives", + "code": "MV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mongolia", + "code": "MN", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Myanmar", + "code": "MM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nepal", + "code": "NP", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Oman", + "code": "OM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Pakistan", + "code": "PK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Palestine, State of", + "code": "PS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Philippines", + "code": "PH", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Qatar", + "code": "QA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saudi Arabia", + "code": "SA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Singapore", + "code": "SG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sri Lanka", + "code": "LK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Syrian Arab Republic", + "code": "SY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Taiwan, Province of China", + "code": "TW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tajikistan", + "code": "TJ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Thailand", + "code": "TH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Timor-Leste", + "code": "TL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Turkey", + "code": "TR", + "id_types": [ + { "type": "ADDRESS_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Turkmenistan", + "code": "TM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "United Arab Emirates", + "code": "AE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Uzbekistan", + "code": "UZ", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Viet Nam", + "code": "VN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Yemen", + "code": "YE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "Europe", + "countries": [ + { + "name": "Albania", + "code": "AL", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Andorra", + "code": "AD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Austria", + "code": "AT", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Belarus", + "code": "BY", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Belgium", + "code": "BE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bosnia and Herzegovina", + "code": "BA", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bulgaria", + "code": "BG", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Croatia", + "code": "HR", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Czech Republic", + "code": "CZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Denmark", + "code": "DK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Estonia", + "code": "EE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Faroe Islands", + "code": "FO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Finland", + "code": "FI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "France", + "code": "FR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRADE_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Germany", + "code": "DE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Gibraltar", + "code": "GI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Greece", + "code": "GR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guernsey", + "code": "GG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Holy See (Vatican City State)", + "code": "VA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Hungary", + "code": "HU", + "id_types": [ + { "type": "ADDRESS_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Iceland", + "code": "IS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ireland", + "code": "IE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Isle of Man", + "code": "IM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Italy", + "code": "IT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Jersey", + "code": "JE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Latvia", + "code": "LV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Liechtenstein", + "code": "LI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Lithuania", + "code": "LT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Luxembourg", + "code": "LU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Malta", + "code": "MT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Moldova, Republic of", + "code": "MD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Monaco", + "code": "MC", + "id_types": [ + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Montenegro", + "code": "ME", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Netherlands", + "code": "NL", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "North Macedonia, Republic of", + "code": "MK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Norway", + "code": "NO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Poland", + "code": "PL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VEHICLE_PARTICULARS", "verification_method": "doc_verification" } + ] + }, + { + "name": "Portugal", + "code": "PT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Romania", + "code": "RO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Russian Federation", + "code": "RU", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VEHICLE_PARTICULARS", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "San Marino", + "code": "SM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Serbia", + "code": "RS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Slovakia", + "code": "SK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Slovenia", + "code": "SI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Spain", + "code": "ES", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sweden", + "code": "SE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Switzerland", + "code": "CH", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ukraine", + "code": "UA", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "United Kingdom", + "code": "GB", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "North America", + "countries": [ + { + "name": "Anguilla", + "code": "AI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Antigua and Barbuda", + "code": "AG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Aruba", + "code": "AW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bahamas", + "code": "BS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Barbados", + "code": "BB", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Belize", + "code": "BZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bermuda", + "code": "BM", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bonaire, Sint Eustatius and Saba", + "code": "BQ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Canada", + "code": "CA", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cayman Islands", + "code": "KY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Costa Rica", + "code": "CR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cuba", + "code": "CU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Curacao", + "code": "CW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Dominica", + "code": "DM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Dominican Republic", + "code": "DO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "El Salvador", + "code": "SV", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Greenland", + "code": "GL", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Grenada", + "code": "GD", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guatemala", + "code": "GT", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Haiti", + "code": "HT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Honduras", + "code": "HN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Jamaica", + "code": "JM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Mexico", + "code": "MX", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Montserrat", + "code": "MS", + "id_types": [ + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nicaragua", + "code": "NI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Panama", + "code": "PA", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Puerto Rico", + "code": "PR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Kitts and Nevis", + "code": "KN", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Lucia", + "code": "LC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Martin (French part)", + "code": "MF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Saint Vincent and the Grenadines", + "code": "VC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Sint Maarten (Dutch part)", + "code": "SX", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" } + ] + }, + { + "name": "Trinidad and Tobago", + "code": "TT", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Turks and Caicos Islands", + "code": "TC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "United States", + "code": "US", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "SOCIAL_ID", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "TRIBAL_CARD", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Virgin Islands, British", + "code": "VG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Virgin Islands, U.S.", + "code": "VI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "Oceania", + "countries": [ + { + "name": "American Samoa", + "code": "AS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Australia", + "code": "AU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Cook Islands", + "code": "CK", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Fiji", + "code": "FJ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TAX_ID", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "French Polynesia", + "code": "PF", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guam", + "code": "GU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Kiribati", + "code": "KI", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Marshall Islands", + "code": "MH", + "id_types": [ + { "type": "CITIZEN_ID", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Micronesia, Federated States of", + "code": "FM", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Nauru", + "code": "NR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "New Zealand", + "code": "NZ", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "PROVISIONAL_DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Niue", + "code": "NU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Northern Mariana Islands", + "code": "MP", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" } + ] + }, + { + "name": "Palau", + "code": "PW", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Papua New Guinea", + "code": "PG", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Samoa", + "code": "WS", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Solomon Islands", + "code": "SB", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tonga", + "code": "TO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Tuvalu", + "code": "TV", + "id_types": [ + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Vanuatu", + "code": "VU", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" } + ] + } + ] + }, + { + "name": "South America", + "countries": [ + { + "name": "Argentina", + "code": "AR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VEHICLE_PARTICULARS", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Bolivia, Plurinational State of", + "code": "BO", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "SEAMANS_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Brazil", + "code": "BR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Chile", + "code": "CL", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Colombia", + "code": "CO", + "id_types": [ + { "type": "ALIEN_CARD", "verification_method": "doc_verification" }, + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "HEALTH_CARD", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "UNIFORMED_SERVICES_CARD", "verification_method": "doc_verification" }, + { "type": "WORK_PERMIT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Ecuador", + "code": "EC", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" }, + { "type": "VOTER_ID", "verification_method": "doc_verification" } + ] + }, + { + "name": "Guyana", + "code": "GY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" } + ] + }, + { + "name": "Paraguay", + "code": "PY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Peru", + "code": "PE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "OCCUPATION_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "RESIDENT_ID", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Suriname", + "code": "SR", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Uruguay", + "code": "UY", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "REGISTRATION_CERTIFICATE", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + }, + { + "name": "Venezuela, Bolivarian Republic of", + "code": "VE", + "id_types": [ + { "type": "DRIVERS_LICENSE", "verification_method": "doc_verification" }, + { "type": "IDENTITY_CARD", "verification_method": "doc_verification" }, + { "type": "PASSPORT", "verification_method": "doc_verification" }, + { "type": "TRAVEL_DOC", "verification_method": "doc_verification" } + ] + } + ] + } + ] + } + \ No newline at end of file diff --git a/app/api/kyc/smile-id/route.ts b/app/api/kyc/smile-id/route.ts new file mode 100644 index 00000000..a8d87b26 --- /dev/null +++ b/app/api/kyc/smile-id/route.ts @@ -0,0 +1,203 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabaseAdmin } from "@/app/lib/supabase"; +import { submitSmileIDJob, type SmileIDIdInfo } from "@/app/lib/smileID"; +import { rateLimit } from "@/app/lib/rate-limit"; + +export async function POST(request: NextRequest) { + // Rate limit check + const rateLimitResult = await rateLimit(request); + if (!rateLimitResult.success) { + return NextResponse.json( + { + status: "error", + message: "Too many requests. Please try again later.", + }, + { status: 429 }, + ); + } + + // Get the wallet address from the header set by the middleware + const walletAddress = request.headers.get("x-wallet-address"); + + if (!walletAddress) { + return NextResponse.json( + { status: "error", message: "Unauthorized" }, + { status: 401 }, + ); + } + + try { + const body = await request.json(); + const { images, partner_params, id_info, email } = body; + + // Validate required fields + if (!images || !Array.isArray(images) || images.length === 0) { + return NextResponse.json( + { status: "error", message: "Invalid images data" }, + { status: 400 }, + ); + } + + // Validate id_info for Job Type 1 (Biometric KYC) + if (!id_info?.country || !id_info?.id_type) { + return NextResponse.json( + { + status: "error", + message: "Missing id_info: country and id_type are required", + }, + { status: 400 }, + ); + } + + // Use server utility to submit SmileID job + type SmileIdResultType = { + job_complete: boolean; + id_info?: any; + [key: string]: any; + }; + + let smileIdResult: SmileIdResultType = { job_complete: false }, + job_id: string, + user_id: string; + try { + const result = await submitSmileIDJob({ + images, + partner_params, + walletAddress, + id_info: id_info as SmileIDIdInfo, + }); + smileIdResult = { job_complete: false, ...result.smileIdResult }; + job_id = result.job_id; + user_id = result.user_id; + } catch (err) { + return NextResponse.json( + { + status: "error", + message: err instanceof Error ? err.message : "SmileID job failed", + }, + { status: 500 }, + ); + } + + // Enhanced KYC (Job Type 5) returns Actions.Verify_ID_Number + // Biometric KYC (Job Type 1) returns job_complete and job_success + const actions = smileIdResult?.Actions; + const isEnhancedKyc = actions?.Verify_ID_Number !== undefined; + const isBiometricKyc = smileIdResult?.job_complete !== undefined; + + let verificationSuccess = false; + + if (isEnhancedKyc) { + // Enhanced KYC: Check if ID verification passed + verificationSuccess = actions.Verify_ID_Number === "Verified"; + } else if (isBiometricKyc) { + // Biometric KYC: Check job_complete and job_success + verificationSuccess = + smileIdResult.job_complete && smileIdResult.job_success; + } + + if (!verificationSuccess) { + const errorMessage = + smileIdResult?.ResultText || "SmileID verification failed"; + return NextResponse.json( + { + status: "error", + message: errorMessage, + }, + { status: 400 }, + ); + } + + // Extract ID info from Smile ID response if available + const smileIdInfo = smileIdResult?.id_info || {}; + + const { data: existingProfile } = await supabaseAdmin + .from("user_kyc_profiles") + .select("platform, tier") + .eq("wallet_address", walletAddress) + .single(); + + const existingPlatform = Array.isArray(existingProfile?.platform) + ? existingProfile.platform + : []; + const otherVerifications = existingPlatform.filter( + (p: { type: string }) => p.type !== "id", + ); + const updatedPlatform = [ + ...otherVerifications, + { + type: "id", + identifier: "smile_id", + reference: job_id, + verified: true, + }, + ]; + + // Prevent tier downgrade — only upgrade to 2 if current tier is lower + const currentTier = Number(existingProfile?.tier) || 0; + const newTier = Math.max(currentTier, 2); + + const { data: updatedProfile, error: supabaseError } = await supabaseAdmin + .from("user_kyc_profiles") + .update({ + // Email from user's Privy profile (if provided) + ...(email && { email_address: email }), + // ID Document fields from id_info or Smile ID response + id_type: id_info.id_type, + id_number: smileIdInfo.id_number || id_info.id_number, + id_country: id_info.country, + // Personal info from Smile ID response + full_name: + smileIdInfo.full_name || + (smileIdInfo.first_name && smileIdInfo.last_name + ? `${smileIdInfo.first_name} ${smileIdInfo.last_name}` + : null), + date_of_birth: smileIdInfo.dob || id_info.dob || null, + platform: updatedPlatform, + verified: true, + verified_at: new Date().toISOString(), + tier: newTier, + }) + .eq("wallet_address", walletAddress) + .select("wallet_address"); + + if (supabaseError) { + return NextResponse.json( + { + status: "error", + message: "Failed to save KYC data", + }, + { status: 500 }, + ); + } + + // Verify that a row was actually updated + if (!updatedProfile || updatedProfile.length === 0) { + return NextResponse.json( + { + status: "error", + message: + "No KYC profile exists. Please complete phone verification first.", + }, + { status: 404 }, + ); + } + + return NextResponse.json({ + status: "success", + message: "KYC verification submitted and saved successfully", + data: { + jobId: job_id, + userId: user_id, + }, + }); + } catch (error) { + return NextResponse.json( + { + status: "error", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 }, + ); + } +} diff --git a/app/api/kyc/status/route.ts b/app/api/kyc/status/route.ts new file mode 100644 index 00000000..2823e9a7 --- /dev/null +++ b/app/api/kyc/status/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabaseAdmin } from "@/app/lib/supabase"; +import { + trackApiRequest, + trackApiResponse, + trackApiError, +} from "@/app/lib/server-analytics"; + +export async function GET(request: NextRequest) { + const startTime = Date.now(); + + try { + trackApiRequest(request, "/api/kyc/status", "GET"); + + // Get the wallet address from the header set by the middleware + const walletAddress = request.headers.get("x-wallet-address"); + + if (!walletAddress) { + trackApiError( + request, + "/api/kyc/status", + "GET", + new Error("Unauthorized"), + 401, + ); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); + } + + // Check KYC profile for phone and SmileID verification status + const { data: kycProfile } = await supabaseAdmin + .from("user_kyc_profiles") + .select("verified, phone_number, tier") + .eq("wallet_address", walletAddress) + .single(); + + const tier: 0 | 1 | 2 | 3 | 4 = + (kycProfile?.tier as 0 | 1 | 2 | 3 | 4) || 0; + const phoneNumber = kycProfile?.phone_number || null; + const phoneVerified = kycProfile?.verified && phoneNumber ? true : false; + + const responseTime = Date.now() - startTime; + trackApiResponse("/api/kyc/status", "GET", 200, responseTime); + + return NextResponse.json({ + success: true, + tier, + isPhoneVerified: phoneVerified, + phoneNumber, + }); + } catch (error) { + trackApiError(request, '/api/kyc/status', 'GET', error as Error, 500); + + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/app/api/kyc/tier3-verify/route.ts b/app/api/kyc/tier3-verify/route.ts new file mode 100644 index 00000000..1b4fcba1 --- /dev/null +++ b/app/api/kyc/tier3-verify/route.ts @@ -0,0 +1,229 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabaseAdmin } from "@/app/lib/supabase"; +import { + verifyUtilityBill, + isDojahVerificationSuccess, +} from "@/app/lib/dojah"; +import { rateLimit } from "@/app/lib/rate-limit"; + +const KYC_BUCKET = process.env.KYC_DOCUMENTS_BUCKET || "kyc-documents"; +const SIGNED_URL_EXPIRY_SEC = 3600; +const MAX_FILE_BYTES = 5 * 1024 * 1024; // 5MB +const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"] as const; +const MIME_TO_EXT: Record = { + "image/jpeg": "jpg", + "image/png": "png", + "image/webp": "webp", +}; + +export async function POST(request: NextRequest) { + const rateLimitResult = await rateLimit(request); + if (!rateLimitResult.success) { + return NextResponse.json( + { success: false, error: "Too many requests. Please try again later." }, + { status: 429 } + ); + } + + const walletAddress = request.headers.get("x-wallet-address"); + if (!walletAddress) { + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 } + ); + } + + try { + const formData = await request.formData(); + const file = formData.get("file") as File | null; + const countryCode = formData.get("countryCode") as string | null; + const documentType = formData.get("documentType") as string | null; + const houseNumber = formData.get("houseNumber") as string | null; + const streetAddress = formData.get("streetAddress") as string | null; + const county = formData.get("county") as string | null; + const postalCode = formData.get("postalCode") as string | null; + + if (!file || file.size === 0) { + return NextResponse.json( + { success: false, error: "Document file is required" }, + { status: 400 } + ); + } + if (!countryCode?.trim()) { + return NextResponse.json( + { success: false, error: "Country is required" }, + { status: 400 } + ); + } + + if (file.size > MAX_FILE_BYTES) { + return NextResponse.json( + { success: false, error: "File too large; maximum 5 MB" }, + { status: 413 } + ); + } + const mime = (file.type || "").toLowerCase(); + if (!ALLOWED_MIME_TYPES.includes(mime as (typeof ALLOWED_MIME_TYPES)[number])) { + return NextResponse.json( + { + success: false, + error: "Invalid file type; allowed: image/jpeg, image/png, image/webp", + }, + { status: 400 } + ); + } + + const nameExt = file.name?.split(".").pop(); + const ext = (nameExt && nameExt.length <= 4 ? nameExt : MIME_TO_EXT[mime]) || "jpg"; + const path = `tier3/${walletAddress}/${Date.now()}.${ext}`; + + const buffer = Buffer.from(await file.arrayBuffer()); + + const { error: uploadError } = await supabaseAdmin.storage + .from(KYC_BUCKET) + .upload(path, buffer, { + contentType: file.type || "image/jpeg", + upsert: false, + }); + + if (uploadError) { + return NextResponse.json( + { + success: false, + error: + uploadError.message || + "Failed to upload document. Ensure the KYC storage bucket exists.", + }, + { status: 500 } + ); + } + + const { data: signedUrlData, error: signError } = + await supabaseAdmin.storage + .from(KYC_BUCKET) + .createSignedUrl(path, SIGNED_URL_EXPIRY_SEC); + + const signedUrl = signedUrlData?.signedUrl; + if (signError || !signedUrl) { + return NextResponse.json( + { + success: false, + error: "Failed to generate document URL", + }, + { status: 500 } + ); + } + + const dojahResult = await verifyUtilityBill(signedUrl); + if (!isDojahVerificationSuccess(dojahResult)) { + const msg = + dojahResult?.entity?.result?.message || + "Document could not be verified as a valid proof of address."; + return NextResponse.json( + { success: false, error: msg }, + { status: 400 } + ); + } + + const { data: currentProfile, error: fetchError } = await supabaseAdmin + .from("user_kyc_profiles") + .select("tier, platform") + .eq("wallet_address", walletAddress) + .single(); + + if (fetchError || !currentProfile) { + return NextResponse.json( + { + success: false, + error: + "No KYC profile found. Complete phone and ID verification first.", + }, + { status: 404 } + ); + } + + const currentTier = Number(currentProfile.tier) ?? 0; + if (currentTier < 2) { + return NextResponse.json( + { + success: false, + error: + "Complete Tier 1 (phone) and Tier 2 (ID) verification before upgrading to Tier 3.", + }, + { status: 400 } + ); + } + + const existingPlatform = Array.isArray(currentProfile?.platform) + ? currentProfile.platform + : []; + const otherVerifications = existingPlatform.filter( + (p: { type: string }) => p.type !== "address" + ); + const updatedPlatform = [ + ...otherVerifications, + { + type: "address", + identifier: "dojah", + verified: true, + }, + ]; + + const updatePayload: Record = { + tier: Math.max(currentTier, 3), + verified: true, + verified_at: new Date().toISOString(), + platform: updatedPlatform, + address_country: countryCode, + address_postal_code: postalCode?.trim() || null, + updated_at: new Date().toISOString(), + }; + if (houseNumber?.trim()) + updatePayload.address_street = [houseNumber, streetAddress?.trim()] + .filter(Boolean) + .join(" "); + else if (streetAddress?.trim()) + updatePayload.address_street = streetAddress.trim(); + if (county?.trim()) updatePayload.address_state = county.trim(); + + const { data: updatedProfile, error: supabaseError } = await supabaseAdmin + .from("user_kyc_profiles") + .update(updatePayload) + .eq("wallet_address", walletAddress) + .select("wallet_address"); + + if (supabaseError) { + return NextResponse.json( + { + success: false, + error: "Failed to update KYC profile", + }, + { status: 500 } + ); + } + + if (!updatedProfile || updatedProfile.length === 0) { + return NextResponse.json( + { + success: false, + error: + "No KYC profile found. Complete phone and ID verification first.", + }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: "Tier 3 address verification completed", + data: { tier: 3 }, + }); + } catch (err) { + const message = + err instanceof Error ? err.message : "Verification failed"; + return NextResponse.json( + { success: false, error: message }, + { status: 500 } + ); + } +} diff --git a/app/api/kyc/transaction-summary/route.ts b/app/api/kyc/transaction-summary/route.ts new file mode 100644 index 00000000..2961450d --- /dev/null +++ b/app/api/kyc/transaction-summary/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from "next/server"; +import { supabaseAdmin } from "@/app/lib/supabase"; +import { + trackApiRequest, + trackApiResponse, + trackApiError, +} from "@/app/lib/server-analytics"; + +export async function GET(request: NextRequest) { + const startTime = Date.now(); + + try { + trackApiRequest(request, "/api/kyc/transaction-summary", "GET"); + + // Get the wallet address from the header set by the middleware + const walletAddress = request.headers.get("x-wallet-address"); + + if (!walletAddress) { + trackApiError( + request, + "/api/kyc/transaction-summary", + "GET", + new Error("Unauthorized"), + 401, + ); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); + } + + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); + + // Fetch transactions for the current month + const { data: transactions, error } = await supabaseAdmin + .from("transactions") + .select("amount_sent, created_at") + .eq("wallet_address", walletAddress) + .eq("transaction_type", "swap") + .in("status", ["fulfilling", "completed"]) + .gte("created_at", monthStart.toISOString()); + + if (error) { + trackApiError(request, "/api/kyc/transaction-summary", "GET", error, 500); + return NextResponse.json( + { success: false, error: "Failed to fetch transaction summary" }, + { status: 500 }, + ); + } + + let dailySpent = 0; + let monthlySpent = 0; + let lastTransactionDate: string | null = null; + + transactions?.forEach((tx) => { + const txDate = new Date(tx.created_at); + const amount = parseFloat(tx.amount_sent) || 0; + + monthlySpent += amount; + + if (txDate >= today) { + dailySpent += amount; + } + + if (!lastTransactionDate || txDate > new Date(lastTransactionDate)) { + lastTransactionDate = tx.created_at; + } + }); + + const responseTime = Date.now() - startTime; + trackApiResponse("/api/kyc/transaction-summary", "GET", 200, responseTime); + + return NextResponse.json({ + success: true, + dailySpent, + monthlySpent, + lastTransactionDate, + }); + } catch (error) { + trackApiError( + request, + "/api/kyc/transaction-summary", + "GET", + error as Error, + 500, + ); + + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/app/api/phone/send-otp/route.ts b/app/api/phone/send-otp/route.ts new file mode 100644 index 00000000..8385e735 --- /dev/null +++ b/app/api/phone/send-otp/route.ts @@ -0,0 +1,179 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createHash } from "crypto"; +import { supabaseAdmin } from "@/app/lib/supabase"; +import { + validatePhoneNumber, + sendKudiSMSOTP, + sendTwilioVerifyOTP, + generateOTP, +} from "../../../lib/phone-verification"; +import { + trackApiRequest, + trackApiResponse, + trackApiError, +} from "../../../lib/server-analytics"; +import { rateLimit } from "@/app/lib/rate-limit"; + +function hashOTP(otp: string): string { + return createHash("sha256").update(otp).digest("hex"); +} + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + + try { + // Rate limit check + const rateLimitResult = await rateLimit(request); + if (!rateLimitResult.success) { + return NextResponse.json( + { success: false, error: "Too many requests. Please try again later." }, + { status: 429 }, + ); + } + + trackApiRequest(request, "/api/phone/send-otp", "POST"); + + const body = await request.json(); + const { phoneNumber, name } = body; + + // Use authenticated wallet address and user ID from middleware + const walletAddress = request.headers.get("x-wallet-address"); + const userId = request.headers.get("x-user-id"); + + if (!walletAddress) { + trackApiError( + request, + "/api/phone/send-otp", + "POST", + new Error("Unauthorized"), + 401, + ); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); + } + + if (!phoneNumber) { + trackApiError( + request, + "/api/phone/send-otp", + "POST", + new Error("Missing required fields"), + 400, + ); + return NextResponse.json( + { + success: false, + error: "Phone number is required", + }, + { status: 400 }, + ); + } + + // Validate phone number + const validation = validatePhoneNumber(phoneNumber); + if (!validation.isValid) { + trackApiError( + request, + "/api/phone/send-otp", + "POST", + new Error("Invalid phone number format"), + 400, + ); + return NextResponse.json( + { success: false, error: "Invalid phone number format" }, + { status: 400 }, + ); + } + + // Get existing profile to preserve important fields + const { data: existingProfile } = await supabaseAdmin + .from("user_kyc_profiles") + .select( + "tier, verified, verified_at, id_country, id_type, platform, full_name", + ) + .eq("wallet_address", walletAddress) + .single(); + + const isNigerian = validation.isNigerian; + const expiresAt = new Date(Date.now() + (isNigerian ? 5 : 10) * 60 * 1000); // 5 min KudiSMS, 10 min Twilio Verify + + // Nigerian: we generate OTP, hash it, and store the hash. Non-Nigerian: Twilio Verify sends its own code. + const otp = isNigerian ? generateOTP() : null; + const otpHash = otp ? hashOTP(otp) : null; + + // Store verification record (otp_code hash only for Nigerian/KudiSMS path) + const { error: dbError } = await supabaseAdmin + .from("user_kyc_profiles") + .upsert( + { + wallet_address: walletAddress, + user_id: userId, + full_name: name || existingProfile?.full_name || null, + phone_number: validation.e164Format, + otp_code: otpHash, + expires_at: expiresAt.toISOString(), + verified: existingProfile?.verified || false, + verified_at: existingProfile?.verified_at || null, + tier: existingProfile?.tier || 0, + id_country: existingProfile?.id_country || null, + id_type: existingProfile?.id_type || null, + platform: existingProfile?.platform || null, + attempts: 0, + provider: validation.provider, + }, + { + onConflict: "wallet_address", + }, + ); + + if (dbError) { + trackApiError(request, "/api/phone/send-otp", "POST", dbError, 500); + return NextResponse.json( + { success: false, error: "Failed to store verification data" }, + { status: 500 }, + ); + } + + // Nigerian: KudiSMS with our OTP. Non-Nigerian: Twilio Verify (Twilio sends its own code). + let result; + if (isNigerian) { + result = await sendKudiSMSOTP(validation.digitsOnly!, otp!); + } else { + result = await sendTwilioVerifyOTP(validation.e164Format!); + } + + const responseTime = Date.now() - startTime; + trackApiResponse( + "/api/phone/send-otp", + "POST", + result.success ? 200 : 400, + responseTime, + ); + + if (!result.success) { + return NextResponse.json( + { + success: false, + error: result.error || result.message, + }, + { status: 400 }, + ); + } + + return NextResponse.json({ + success: result.success, + message: result.message, + provider: validation.provider, + phoneNumber: validation.internationalFormat, + }); + } catch (error) { + trackApiError(request, "/api/phone/send-otp", "POST", error as Error, 500); + + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/app/api/phone/verify-otp/route.ts b/app/api/phone/verify-otp/route.ts new file mode 100644 index 00000000..9754cad6 --- /dev/null +++ b/app/api/phone/verify-otp/route.ts @@ -0,0 +1,292 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createHash } from "crypto"; +import { supabaseAdmin } from "@/app/lib/supabase"; +import { + trackApiRequest, + trackApiResponse, + trackApiError, +} from "../../../lib/server-analytics"; +import { + validatePhoneNumber, + checkTwilioVerifyCode, +} from "@/app/lib/phone-verification"; + +const MAX_ATTEMPTS = 3; + +function hashOTP(otp: string): string { + return createHash("sha256").update(otp).digest("hex"); +} + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + + try { + trackApiRequest(request, "/api/phone/verify-otp", "POST"); + + const body = await request.json(); + const { phoneNumber, otpCode } = body; + + // Use authenticated wallet address from middleware + const walletAddress = request.headers.get("x-wallet-address"); + + if (!walletAddress) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + new Error("Unauthorized"), + 401, + ); + return NextResponse.json( + { success: false, error: "Unauthorized" }, + { status: 401 }, + ); + } + + if (!phoneNumber || !otpCode) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + new Error("Missing required fields"), + 400, + ); + return NextResponse.json( + { success: false, error: "Phone number and OTP code are required" }, + { status: 400 }, + ); + } + + // Normalize phone number to E.164 format for consistent querying + const validation = validatePhoneNumber(phoneNumber); + if (!validation.isValid || !validation.e164Format) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + new Error("Invalid phone format"), + 400, + ); + return NextResponse.json( + { success: false, error: "Invalid phone number format" }, + { status: 400 }, + ); + } + + // Get verification record using normalized E.164 format + const { data: verification, error: fetchError } = await supabaseAdmin + .from("user_kyc_profiles") + .select("verified, provider, tier, expires_at, attempts, otp_code") + .eq("wallet_address", walletAddress) + .eq("phone_number", validation.e164Format) + .single(); + + if (fetchError) { + trackApiError(request, "/api/phone/verify-otp", "POST", fetchError, 500); + return NextResponse.json( + { success: false, error: "Failed to fetch verification record" }, + { status: 500 }, + ); + } + + if (!verification) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + new Error("Verification not found"), + 404, + ); + return NextResponse.json( + { success: false, error: "Verification record not found" }, + { status: 404 }, + ); + } + + // Check if already verified + if (verification.verified) { + const responseTime = Date.now() - startTime; + trackApiResponse("/api/phone/verify-otp", "POST", 200, responseTime); + return NextResponse.json({ + success: true, + message: "Phone number already verified", + verified: true, + }); + } + + // Twilio Verify path: validate code via Twilio (no DB OTP comparison) + if (verification.provider === "twilio") { + const checkResult = await checkTwilioVerifyCode( + validation.e164Format!, + otpCode, + ); + if (!checkResult.success) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + new Error(checkResult.error || "Verification failed"), + 400, + ); + return NextResponse.json( + { + success: false, + error: + checkResult.error || + "Invalid or expired code. Please try again or request a new code.", + }, + { status: 400 }, + ); + } + // Twilio approved: update profile (same as below) + const updateData: Record = { + verified: true, + verified_at: new Date().toISOString(), + otp_code: null, + attempts: 0, + }; + if (verification.tier === 0) { + updateData.tier = 1; + } + const { error: updateError } = await supabaseAdmin + .from("user_kyc_profiles") + .update(updateData) + .eq("wallet_address", walletAddress); + if (updateError) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + updateError, + 500, + ); + return NextResponse.json( + { success: false, error: "Failed to update verification status" }, + { status: 500 }, + ); + } + const responseTime = Date.now() - startTime; + trackApiResponse("/api/phone/verify-otp", "POST", 200, responseTime); + return NextResponse.json({ + success: true, + message: "Phone number verified successfully", + verified: true, + phoneNumber, + }); + } + + // KudiSMS path: expiry, attempts, and DB OTP comparison + if (new Date() > new Date(verification.expires_at)) { + return NextResponse.json( + { success: false, error: "OTP has expired. Please request a new one." }, + { status: 400 }, + ); + } + + if (verification.attempts >= MAX_ATTEMPTS) { + return NextResponse.json( + { + success: false, + error: + "Maximum verification attempts exceeded. Please request a new OTP.", + }, + { status: 429 }, + ); + } + + if (verification.otp_code !== hashOTP(otpCode)) { + // Atomic increment with boundary check to prevent race conditions + const { data: updated, error: attemptsError } = await supabaseAdmin + .from("user_kyc_profiles") + .update({ attempts: verification.attempts + 1 }) + .eq("wallet_address", walletAddress) + .lt("attempts", MAX_ATTEMPTS) + .select("attempts") + .single(); + + if (attemptsError) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + attemptsError, + 500, + ); + return NextResponse.json( + { success: false, error: "Failed to process verification attempt" }, + { status: 500 }, + ); + } + + // If no rows updated, attempts limit was hit mid-flight (race condition) + if (!updated) { + return NextResponse.json( + { + success: false, + error: + "Maximum verification attempts exceeded. Please request a new OTP.", + }, + { status: 429 }, + ); + } + + return NextResponse.json( + { + success: false, + error: "Invalid OTP code", + attemptsRemaining: MAX_ATTEMPTS - updated.attempts, + }, + { status: 400 }, + ); + } + + // Mark as verified - preserve existing tier if higher than 1 + const updateData: Record = { + verified: true, + verified_at: new Date().toISOString(), + otp_code: null, // Clear OTP hash after successful verification + attempts: 0, + }; + + // Only set tier to 1 if current tier is 0 (unverified) + if (verification.tier === 0) { + updateData.tier = 1; + } + + const { error: updateError } = await supabaseAdmin + .from("user_kyc_profiles") + .update(updateData) + .eq("wallet_address", walletAddress); + + if (updateError) { + trackApiError(request, "/api/phone/verify-otp", "POST", updateError, 500); + return NextResponse.json( + { success: false, error: "Failed to update verification status" }, + { status: 500 }, + ); + } + + const responseTime = Date.now() - startTime; + trackApiResponse("/api/phone/verify-otp", "POST", 200, responseTime); + + return NextResponse.json({ + success: true, + message: "Phone number verified successfully", + verified: true, + phoneNumber: phoneNumber, + }); + } catch (error) { + trackApiError( + request, + "/api/phone/verify-otp", + "POST", + error as Error, + 500, + ); + + return NextResponse.json( + { success: false, error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/app/components/KycModal.tsx b/app/components/KycModal.tsx index ac76f03a..d1bf06fa 100644 --- a/app/components/KycModal.tsx +++ b/app/components/KycModal.tsx @@ -1,29 +1,63 @@ "use client"; import { Checkbox, DialogTitle, Field, Label } from "@headlessui/react"; import { toast } from "sonner"; -import { QRCode } from "react-qrcode-logo"; + import { usePrivy, useWallets } from "@privy-io/react-auth"; -import { FiExternalLink } from "react-icons/fi"; import { motion, AnimatePresence } from "framer-motion"; -import { useState, useCallback, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; +declare global { + namespace JSX { + interface IntrinsicElements { + "smart-camera-web": any; + } + } + interface Window { + tf?: { + load: () => void; + reload: () => void; + }; + } +} import { CheckIcon, - QrCodeIcon, SadFaceIcon, UserDetailsIcon, VerificationPendingIcon, } from "./ImageAssets"; +import { DropdownItem, FlexibleDropdown } from "./FlexibleDropdown"; +import { + ArrowDown01Icon, + ArrowLeft01Icon, + FileAddIcon, + Folder02Icon, + MapPinpoint01Icon, + PencilEdit01Icon, + PencilEdit02Icon, + Tick01Icon, +} from "hugeicons-react"; +import { classNames } from "../utils"; import { fadeInOut } from "./AnimatedComponents"; -import { generateTimeBasedNonce } from "../utils"; -import { fetchKYCStatus, initiateKYC } from "../api/aggregator"; +import { fetchKYCStatus, submitSmileIDData } from "../api/aggregator"; import { primaryBtnClasses, secondaryBtnClasses } from "./Styles"; import { trackEvent } from "../hooks/analytics/client"; -import { Cancel01Icon, CheckmarkCircle01Icon } from "hugeicons-react"; +import { CheckmarkCircle01Icon, Clock05Icon, StarIcon } from "hugeicons-react"; import { useInjectedWallet } from "../context"; +import { KYC_TIERS, useKYC } from "../context/KYCContext"; +import { formatNumberWithCommas } from "../utils"; +import { DocumentRequirementsModal } from "./kyc/DocumentRequirementsModal"; + +import idTypesData from "../api/kyc/smile-id/id_types.json"; + +const TIER3_DOCUMENT_TYPES = [ + { value: "utility_bill", label: "Utility bill" }, + { value: "bank_statement", label: "Bank statement" }, +] as const; export const STEPS = { TERMS: "terms", + ID_INFO: "id_info", + CAPTURE: "capture", STATUS: { PENDING: "pending", SUCCESS: "success", @@ -32,25 +66,65 @@ export const STEPS = { LOADING: "loading", EXPIRED: "expired", REFRESH: "refresh", + // Tier 3 (address verification) flow + TIER3_PROMPT: "tier3_prompt", + TIER3_COUNTRY: "tier3_country", + TIER3_UPLOAD: "tier3_upload", + // Tier 4 (business verification) flow + TIER4_TYPEFORM: "tier4_typeform", } as const; type Step = | typeof STEPS.TERMS + | typeof STEPS.ID_INFO + | typeof STEPS.CAPTURE | typeof STEPS.LOADING | typeof STEPS.REFRESH - | (typeof STEPS.STATUS)[keyof typeof STEPS.STATUS]; + | (typeof STEPS.STATUS)[keyof typeof STEPS.STATUS] + | typeof STEPS.TIER3_PROMPT + | typeof STEPS.TIER3_COUNTRY + | typeof STEPS.TIER3_UPLOAD + | typeof STEPS.TIER4_TYPEFORM; + +// Types for ID types JSON +type IdType = { + type: string; + verification_method: string; +}; + +type Country = { + name: string; + code: string; + id_types: IdType[]; +}; + +const getAllCountries = (): Country[] => { + return idTypesData.continents + .flatMap((continent) => continent.countries) + .sort((a, b) => a.name.localeCompare(b.name)); +}; + +const requiresDocumentCapture = ( + country: Country | null, + idType: string, +): boolean => { + if (!country || !idType) return true; + const selectedIdType = country.id_types.find((t) => t.type === idType); + return selectedIdType?.verification_method === "doc_verification"; +}; export const KycModal = ({ setIsUserVerified, setIsKycModalOpen, + targetTier, }: { setIsUserVerified: (value: boolean) => void; setIsKycModalOpen: (value: boolean) => void; + targetTier?: 2 | 3 | 4; }) => { - const { signMessage } = usePrivy(); + const { getAccessToken, user } = usePrivy(); const { wallets } = useWallets(); - const { isInjectedWallet, injectedAddress, injectedProvider } = - useInjectedWallet(); + const { isInjectedWallet, injectedAddress } = useInjectedWallet(); const embeddedWallet = wallets.find( (wallet) => wallet.walletClientType === "privy", @@ -59,126 +133,82 @@ export const KycModal = ({ ? injectedAddress : embeddedWallet?.address; - const [step, setStep] = useState(STEPS.LOADING); - const [showQRCode, setShowQRCode] = useState(false); - const [kycUrl, setKycUrl] = useState(""); + const [step, setStep] = useState(() => + targetTier === 4 + ? STEPS.TIER4_TYPEFORM + : targetTier === 3 + ? STEPS.TIER3_PROMPT + : STEPS.LOADING, + ); const [termsAccepted, setTermsAccepted] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); - const [isSigning, setIsSigning] = useState(false); - - const handleSignAndContinue = async () => { - setIsSigning(true); - const nonce = generateTimeBasedNonce({ length: 16 }); - const message = `I accept the KYC Policy and hereby request an identity verification check for ${walletAddress} with nonce ${nonce}`; - - try { - let signature: string; - - if (isInjectedWallet && injectedProvider) { - try { - const accounts = await injectedProvider.request({ - method: "eth_requestAccounts", - }); - - const signResult = await injectedProvider.request({ - method: "personal_sign", - params: [`0x${Buffer.from(message).toString("hex")}`, accounts[0]], - }); - - signature = signResult; - } catch (error) { - console.error("Injected wallet signature error:", error); - toast.error("Failed to sign message with injected wallet"); - setIsSigning(false); - return; - } - } else { - const signResult = await signMessage( - { message }, - { uiOptions: { buttonText: "Sign" } }, - ); - - if (!signResult) { - setIsSigning(false); - return; - } - - signature = signResult.signature; - } - - if (signature) { - setIsKycModalOpen(true); - setStep(STEPS.LOADING); - - const sigWithoutPrefix = signature.startsWith("0x") - ? signature.slice(2) - : signature; + const [cameraElement, setCameraElement] = useState(null); + const [smileIdLoaded, setSmileIdLoaded] = useState(false); + + // ID info state for Job Type 1 + const [selectedCountry, setSelectedCountry] = useState(null); + const [selectedIdType, setSelectedIdType] = useState(""); + const [idNumber, setIdNumber] = useState(""); + + // Tier 3 (address verification) state + const [tier3CountryCode, setTier3CountryCode] = useState(""); + const [tier3HouseNumber, setTier3HouseNumber] = useState(""); + const [tier3StreetAddress, setTier3StreetAddress] = useState(""); + const [tier3County, setTier3County] = useState(""); + const [tier3PostalCode, setTier3PostalCode] = useState(""); + const [tier3DocumentType, setTier3DocumentType] = useState( + TIER3_DOCUMENT_TYPES[0].value, + ); + const [tier3UploadedFile, setTier3UploadedFile] = useState(null); + const [tier3RequirementsOpen, setTier3RequirementsOpen] = useState(false); + const [tier3Submitting, setTier3Submitting] = useState(false); + const [tier3ErrorMessage, setTier3ErrorMessage] = useState( + null, + ); + const { refreshStatus } = useKYC(); + const countries = getAllCountries(); + const tier3CountryOptions = countries.map((c) => ({ + name: c.code, + label: c.name, + imageUrl: `https://flagcdn.com/h24/${c.code.toLowerCase()}.webp`, + })); + const tier3SelectedCountryLabel = + tier3CountryOptions.find((c) => c.name === tier3CountryCode)?.label ?? ""; + const tier3AddressDisplay = + [ + tier3HouseNumber, + tier3StreetAddress, + tier3County, + tier3PostalCode, + tier3SelectedCountryLabel, + ] + .filter(Boolean) + .join(", ") || "—"; + + // Check if current selection requires document capture or just ID number + const needsDocCapture = requiresDocumentCapture( + selectedCountry, + selectedIdType, + ); - const response = await initiateKYC({ - signature: sigWithoutPrefix, - walletAddress: walletAddress || "", - nonce, + useEffect(() => { + // Only load SmileID components for Tier 2 verification flow + if (targetTier === 3 || targetTier === 4) return; + if (typeof window !== "undefined" && !smileIdLoaded) { + import("@smileid/web-components/smart-camera-web") + .then(() => { + setSmileIdLoaded(true); + }) + .catch(() => { + toast.error("Failed to load verification component"); }); - - if (response.status === "success") { - trackEvent("Account verification", { - "Verification status": "Pending", - }); - setKycUrl(response.data.url); - setShowQRCode(true); - setStep(STEPS.STATUS.PENDING); - } else { - setStep(STEPS.STATUS.FAILED); - trackEvent("Account verification", { - "Verification status": "Failed", - }); - } - } - } catch (error: unknown) { - console.log("error", error); - if ( - error instanceof Error && - (error as any).response && - (error as any).response.data - ) { - // backend error response - const { status, message, data } = (error as any).response.data; - toast.error(`${message}: ${data}`); - } else { - // unexpected errors - toast.error(error instanceof Error ? error.message : String(error)); - } - setIsKycModalOpen(false); - setStep(STEPS.TERMS); - } finally { - setIsSigning(false); } - }; + }, [smileIdLoaded, targetTier]); - const QRCodeComponent = useCallback( - () => ( -
- -
- ), - [kycUrl], - ); + const handleAcceptTerms = () => { + setIsKycModalOpen(true); + setStep(STEPS.ID_INFO); + }; const renderTerms = () => ( @@ -203,7 +233,7 @@ export const KycModal = ({
); - const renderQRCode = () => ( - -
-
-

- Verify with your phone or URL -

+ const renderIdInfo = () => ( + +
+ +
+

+ Select your ID document +

+

+ Choose your country and the type of ID you'll use for + verification. +

+
+
+ +
+ {/* Country Selection */} +
+ + ({ name: c.code, label: c.name }))} + selectedItem={selectedCountry?.code} + onSelect={(code) => { + const country = countries.find((c) => c.code === code); + setSelectedCountry(country || null); + setSelectedIdType(""); + setIdNumber(""); + }} + mobileTitle="Select Country" + dropdownWidth={350} + > + {({ selectedItem, isOpen, toggleDropdown }) => ( + + )} + +
+ + {/* ID Type Selection */} + {selectedCountry && ( +
+ + ({ + name: t.type, + label: t.type.replace(/_/g, " "), + }))} + selectedItem={selectedIdType} + onSelect={(type) => { + setSelectedIdType(type); + setIdNumber(""); + }} + mobileTitle="Select ID Type" + dropdownWidth={350} + > + {({ selectedItem, isOpen, toggleDropdown }) => ( + + )} + +
+ )} + + {/* ID Number Input - only for biometric_kyc verification (BVN, NIN, etc.) */} + {selectedIdType && !needsDocCapture && ( +
+ + setIdNumber(e.target.value)} + placeholder={`Enter your ${selectedIdType.replace(/_/g, " ")} number`} + className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-lavender-500 dark:border-white/10 dark:bg-white/5 dark:text-white" + /> +

+ Your ID will be verified against the government database +

+
+ )} +
+ +
+
+
+ ); -

- Scan with your phone to have the best verification experience. You can - also open the URL below -

- - + const renderCapture = () => ( + +
+ +
+

+ {needsDocCapture ? "Capture your documents" : "Take a selfie"} +

+

+ {needsDocCapture + ? "Please take a selfie and capture your ID document for verification." + : "Please take a selfie to verify your identity against your ID."} +

+
+
-
-
-

Or

-
+
+ {needsDocCapture ? ( + /* @ts-expect-error - SmileID web component */ + setCameraElement(el)} + theme-color="#8B85F4" + capture-id + /> + ) : ( + /* @ts-expect-error - SmileID web component */ + setCameraElement(el)} + theme-color="#8B85F4" + /> + )}
); const renderPendingStatus = () => ( - - + + -
+
- Verification in progress + Tier 2 Upgrade in progress

- We are verifying your identity. This will only take a few minutes. - Kindly check back soon + We are currently verifying your identity. You will get feedback within + 24 hours. Kindly check back soon

- @@ -392,10 +557,10 @@ export const KycModal = ({ ); const renderSuccessStatus = () => ( - - + + -
+
Verification successful @@ -409,7 +574,8 @@ export const KycModal = ({ + +
+ + ); + }; + + const renderTier3Country = () => ( + +
+ +
+

+ Get started with Tier 3 KYC Upgrade +

+

+ Fill the details below to start upgrade +

+
+
+
+ +
+ + setTier3CountryCode(code)} + mobileTitle="Select Country" + dropdownWidth={350} + > + {({ selectedItem, isOpen, toggleDropdown }) => ( + + )} + +
+ {tier3CountryCode != "" && ( +
+
+ + setTier3HouseNumber(e.target.value)} + placeholder="Enter house number" + className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-left font-light text-gray-900 focus:outline-none focus:ring-2 focus:ring-[#A9A9BC] dark:border-white/10 dark:bg-transparent dark:text-white/50 dark:placeholder:text-white/50" + /> +
+
+ + setTier3StreetAddress(e.target.value)} + placeholder="Enter street address" + className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-left font-light text-gray-900 focus:outline-none focus:ring-2 focus:ring-[#A9A9BC] dark:border-white/10 dark:bg-transparent dark:text-white/50 dark:placeholder:text-white/50" + /> +
+
+ + setTier3County(e.target.value)} + placeholder="Select county" + className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-left font-light text-gray-900 focus:outline-none focus:ring-2 focus:ring-[#A9A9BC] dark:border-white/10 dark:bg-transparent dark:text-white/50 dark:placeholder:text-white/50" + /> +
+
+ + setTier3PostalCode(e.target.value)} + placeholder="Enter postal code" + className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-left font-light text-gray-900 focus:outline-none focus:ring-2 focus:ring-[#A9A9BC] dark:border-white/10 dark:bg-transparent dark:text-white/50 dark:placeholder:text-white/50" + /> +
+
+ )} +
+
+ + +
+
+ ); + + const ALLOWED_TIER3_EXTENSIONS = ["JPG", "PNG", "PDF", "DOC", "JPEG", "DOCX"]; + const TIER3_MAX_BYTES = 5 * 1024 * 1024; + + const renderTier3Upload = () => { + const handleTier3FileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const ext = file.name.split(".").pop()?.toUpperCase(); + if (!ext || !ALLOWED_TIER3_EXTENSIONS.includes(ext)) { + setTier3ErrorMessage( + "Invalid file type; allowed: JPG, PNG, PDF, DOC, JPEG, DOCX", + ); + return; + } + if (file.size > TIER3_MAX_BYTES) { + setTier3ErrorMessage("File too large; maximum 5 MB"); + return; + } + setTier3ErrorMessage(null); + setTier3UploadedFile(file); + }; + const handleTier3Drop = (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files?.[0]; + if (!file) return; + const ext = file.name.split(".").pop()?.toUpperCase(); + if (!ext || !ALLOWED_TIER3_EXTENSIONS.includes(ext)) { + setTier3ErrorMessage( + "Invalid file type; allowed: JPG, PNG, PDF, DOC, JPEG, DOCX", + ); + return; + } + if (file.size > TIER3_MAX_BYTES) { + setTier3ErrorMessage("File too large; maximum 5 MB"); + return; + } + setTier3ErrorMessage(null); + setTier3UploadedFile(file); + }; + const docLabel = + TIER3_DOCUMENT_TYPES.find( + (d) => d.value === tier3DocumentType, + )?.label?.toLowerCase() ?? "document"; + return ( + + +
+

+ Upload proof of address +

+

+ Provide any of the document below as proof of your residential + address +

+
+
+ +
+

+ Current address{" "} + +

+

+ {tier3AddressDisplay !== "—" ? tier3AddressDisplay : "—"} +

+
+
+ + ({ + name: d.value, + label: d.label, + })) as DropdownItem[] + } + selectedItem={ + TIER3_DOCUMENT_TYPES.find((d) => d.value === tier3DocumentType) + ?.value ?? undefined + } + mobileTitle="Select Document Type" + dropdownWidth={350} + onSelect={(value) => + setTier3DocumentType(value as typeof tier3DocumentType) + } + > + {({ selectedItem, isOpen, toggleDropdown }) => ( + + )} + +
+
+ {tier3DocumentType && ( +
+
+ + +
+
e.preventDefault()} + className={classNames( + "flex flex-col items-start justify-center gap-2 rounded-2xl border-[1.5px] border-dashed border-white/5 bg-transparent px-4 py-3", + tier3UploadedFile ? "border-lavender-500/50" : "", + )} + > +
+ {tier3UploadedFile ? ( + + ) : ( + + )} +
+ {tier3UploadedFile ? ( + <> +

+ {tier3UploadedFile.name}{" "} + {" "} + + change + +

+

+ Size: {(tier3UploadedFile.size / 1024 / 1024).toFixed(2)} MB + Format:{" "} + {String( + tier3UploadedFile.type?.split("/")[1] ?? + tier3UploadedFile.name?.split(".").pop() ?? + "UNKNOWN", + ).toUpperCase()} +

+ + ) : ( + <> +

+ Drag and drop or{" "} + +

+

+ JPG, PNG, PDF, DOC allowed. 5MB Max. +

+ + )} + {tier3ErrorMessage && ( +

+ {tier3ErrorMessage} +

+ )} +
+
+ )} +
+ + +
+
+ ); + }; + const handleRefresh = async () => { setIsRefreshing(true); await fetchStatus(); setIsRefreshing(false); }; - // fetch the KYC status + // Tier 4: Typeform live embed + const typeformContainerRef = useRef(null); + const typeformScriptLoaded = useRef(false); + const [isTypeformReady, setIsTypeformReady] = useState(false); + + useEffect(() => { + if (step !== STEPS.TIER4_TYPEFORM) return; + setIsTypeformReady(false); + + const loadScript = () => { + if (typeformScriptLoaded.current && window.tf) { + window.tf.load(); + return; + } + + const existing = document.querySelector( + 'script[src*="embed.typeform.com/next/embed.js"]', + ); + if (existing) { + typeformScriptLoaded.current = true; + window.tf?.load(); + return; + } + + const script = document.createElement("script"); + script.src = "//embed.typeform.com/next/embed.js"; + script.async = true; + script.onload = () => { + typeformScriptLoaded.current = true; + }; + document.head.appendChild(script); + }; + + const scriptTimer = setTimeout(loadScript, 100); + const spinnerTimer = setTimeout(() => setIsTypeformReady(true), 2000); + + return () => { + clearTimeout(scriptTimer); + clearTimeout(spinnerTimer); + }; + }, [step]); + + const renderTier4Typeform = () => ( + +
+ +
+

+ Business verification +

+

+ Complete this form to apply for unlimited transaction limits. +

+
+
+ + {!isTypeformReady && ( +
+
+
+ )} + +
+ +
+
+ + ); + const fetchStatus = async () => { - if (!walletAddress) return; + if (!walletAddress || targetTier === 3 || targetTier === 4) return; try { const response = await fetchKYCStatus(walletAddress); @@ -505,12 +1200,15 @@ export const KycModal = ({ setStep(newStatus); if (newStatus === STEPS.STATUS.SUCCESS) { - setShowQRCode(false); trackEvent("Account verification", { "Verification status": "Success", }); } - if (newStatus === STEPS.STATUS.PENDING) setKycUrl(response.data.url); + if (newStatus === STEPS.STATUS.PENDING) { + // setKycUrl(response.data.url); + // setIsKycModalOpen(true); + return; + } if (newStatus === STEPS.STATUS.FAILED) { trackEvent("Account verification", { "Verification status": "Failed", @@ -580,20 +1278,141 @@ export const KycModal = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletAddress]); + // Handle Smile ID publish event + useEffect(() => { + if (step !== STEPS.CAPTURE) { + return; + } + + if (!cameraElement) { + return; + } + + const handlePublish = async (event: any) => { + // Show loading screen while submitting + setStep(STEPS.LOADING); + + try { + const { images, partner_params } = event.detail; + + // Validate data structure + if (!images || !Array.isArray(images) || images.length === 0) { + throw new Error("Invalid image data received"); + } + + if (!walletAddress) { + throw new Error("Missing wallet address"); + } + + // Get access token for JWT authentication + const accessToken = await getAccessToken(); + if (!accessToken) { + throw new Error("No access token available"); + } + + // Validate ID info is selected + if (!selectedCountry || !selectedIdType) { + throw new Error("Please select country and ID type"); + } + + // For biometric_kyc (BVN, NIN, etc.), ID number is required + if (!needsDocCapture && !idNumber) { + throw new Error("Please enter your ID number"); + } + + const payload = { + images, + partner_params: { + ...partner_params, + user_id: `user-${walletAddress}`, + }, + id_info: { + country: selectedCountry.code, + id_type: selectedIdType, + ...(idNumber && { id_number: idNumber }), + }, + email: user?.email?.address, + }; + + const response = await submitSmileIDData(payload, accessToken); + + if (response.status === "success") { + setStep(STEPS.STATUS.PENDING); + trackEvent("Account verification", { + "Verification status": "Submitted", + }); + } else { + setStep(STEPS.STATUS.FAILED); + } + } catch (error) { + toast.error("Failed to submit verification data"); + setStep(STEPS.STATUS.FAILED); + } + }; + + const handleCancel = () => { + toast.info("Verification cancelled"); + setStep(STEPS.TERMS); + }; + + const handleBack = () => { + // Handle back navigation if needed + }; + + cameraElement.addEventListener("smart-camera-web.publish", handlePublish); + cameraElement.addEventListener("smart-camera-web.cancelled", handleCancel); + cameraElement.addEventListener("smart-camera-web.back", handleBack); + + return () => { + cameraElement.removeEventListener( + "smart-camera-web.publish", + handlePublish, + ); + cameraElement.removeEventListener( + "smart-camera-web.cancelled", + handleCancel, + ); + cameraElement.removeEventListener("smart-camera-web.back", handleBack); + }; + }, [ + step, + cameraElement, + walletAddress, + selectedCountry, + selectedIdType, + idNumber, + needsDocCapture, + getAccessToken, + user, + ]); + return ( <> - {showQRCode - ? renderQRCode() - : { - [STEPS.TERMS]: renderTerms(), - [STEPS.STATUS.PENDING]: renderPendingStatus(), - [STEPS.STATUS.SUCCESS]: renderSuccessStatus(), - [STEPS.STATUS.FAILED]: renderFailedStatus(), - [STEPS.LOADING]: renderLoadingStatus(), - [STEPS.REFRESH]: renderRefresh(), - }[step]} + { + { + [STEPS.TERMS]: renderTerms(), + [STEPS.ID_INFO]: renderIdInfo(), + [STEPS.CAPTURE]: renderCapture(), + [STEPS.STATUS.PENDING]: renderPendingStatus(), + [STEPS.STATUS.SUCCESS]: renderSuccessStatus(), + [STEPS.STATUS.FAILED]: renderFailedStatus(), + [STEPS.LOADING]: renderLoadingStatus(), + [STEPS.REFRESH]: renderRefresh(), + [STEPS.TIER3_PROMPT]: renderTier3Prompt(), + [STEPS.TIER3_COUNTRY]: renderTier3Country(), + [STEPS.TIER3_UPLOAD]: renderTier3Upload(), + [STEPS.TIER4_TYPEFORM]: renderTier4Typeform(), + }[step] + } + setTier3RequirementsOpen(false)} + addressDisplay={ + tier3AddressDisplay !== "—" ? tier3AddressDisplay : undefined + } + /> ); }; diff --git a/app/components/PhoneVerificationModal.tsx b/app/components/PhoneVerificationModal.tsx new file mode 100644 index 00000000..219c76bc --- /dev/null +++ b/app/components/PhoneVerificationModal.tsx @@ -0,0 +1,610 @@ +"use client"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { toast } from "sonner"; +import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useWallets, usePrivy } from "@privy-io/react-auth"; +import { + Cancel01Icon, + CheckmarkCircle01Icon, + AiPhone01Icon, + Message01Icon, + ArrowDown01Icon, + TelephoneIcon, + InformationSquareIcon, + ArrowLeft02Icon, +} from "hugeicons-react"; +import { parsePhoneNumber } from "libphonenumber-js"; +import { primaryBtnClasses, secondaryBtnClasses } from "./Styles"; +import { fadeInOut, AnimatedComponent, slideInOut } from "./AnimatedComponents"; +import { classNames } from "../utils"; +import { + fetchCountries, + getPopularCountries, + searchCountries, + type Country, +} from "../lib/countries"; + +interface PhoneVerificationModalProps { + isOpen: boolean; + onClose: () => void; + onVerified: (phoneNumber: string) => void; +} + +const STEPS = { + ENTER_PHONE: "enter_phone", + ENTER_OTP: "enter_otp", + VERIFIED: "verified", +} as const; + +type Step = (typeof STEPS)[keyof typeof STEPS]; + +export default function PhoneVerificationModal({ + isOpen, + onClose, + onVerified, +}: PhoneVerificationModalProps) { + const { wallets } = useWallets(); + const { getAccessToken } = usePrivy(); + + const embeddedWallet = wallets.find( + (wallet) => wallet.walletClientType === "privy", + ); + const walletAddress = embeddedWallet?.address; + + const [step, setStep] = useState(STEPS.ENTER_PHONE); + const [name, setName] = useState(""); + const [phoneNumber, setPhoneNumber] = useState(""); + const [formattedPhone, setFormattedPhone] = useState(""); + const [otpCode, setOtpCode] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [provider, setProvider] = useState<"kudisms" | "twilio">("kudisms"); + const [attemptsRemaining, setAttemptsRemaining] = useState(3); + const [selectedCountry, setSelectedCountry] = useState({ + code: "+234", + flag: "https://flagcdn.com/w40/ng.png", + name: "Nigeria", + country: "NG", + }); + const [isCountryDropdownOpen, setIsCountryDropdownOpen] = useState(false); + const [countries, setCountries] = useState([]); + const [filteredCountries, setFilteredCountries] = useState([]); + const [countrySearch, setCountrySearch] = useState(""); + const [isLoadingCountries, setIsLoadingCountries] = useState(false); + const dropdownRef = useRef(null); + + // Load countries when modal opens + useEffect(() => { + if (isOpen && countries.length === 0) { + setIsLoadingCountries(true); + fetchCountries() + .then((data) => { + setCountries(data); + setFilteredCountries(data); + + // Set Nigeria as default if available + const nigeria = data.find((c) => c.country === "NG"); + if (nigeria) { + setSelectedCountry(nigeria); + } + }) + .catch(() => { + toast.error("Failed to load countries. Using defaults."); + }) + .finally(() => { + setIsLoadingCountries(false); + }); + } + }, [isOpen, countries.length]); + + // Filter countries based on search + useEffect(() => { + if (countrySearch.trim()) { + setFilteredCountries(searchCountries(countries, countrySearch)); + } else { + // Show popular countries first, then the rest + const popularCountryCodes = getPopularCountries(); + const popular = countries.filter((c) => + popularCountryCodes.includes(c.country), + ); + const others = countries.filter( + (c) => !popularCountryCodes.includes(c.country), + ); + setFilteredCountries([...popular, ...others]); + } + }, [countrySearch, countries]); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsCountryDropdownOpen(false); + setCountrySearch(""); + } + }; + + if (isCountryDropdownOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + } + }, [isCountryDropdownOpen]); + + const handlePhoneSubmit = useCallback(async () => { + if (!name.trim()) { + toast.error("Please enter your full name"); + return; + } + if (!phoneNumber.trim() || !walletAddress) { + toast.error("Please enter a valid phone number"); + return; + } + + try { + // Combine selected country code with phone number + let fullPhoneNumber = phoneNumber.trim(); + if (!fullPhoneNumber.startsWith("+")) { + // Remove any leading zeros and add selected country code + fullPhoneNumber = fullPhoneNumber.replace(/^0+/, ""); + fullPhoneNumber = selectedCountry.code + fullPhoneNumber; + } + + // Validate phone number format + const parsed = parsePhoneNumber(fullPhoneNumber); + if (!parsed || !parsed.isValid()) { + toast.error("Please enter a valid phone number"); + return; + } + + setIsLoading(true); + + const accessToken = await getAccessToken(); + const response = await fetch("/api/phone/send-otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + phoneNumber: fullPhoneNumber, + name: name, + }), + }); + + const data = await response.json(); + + if (data.success) { + setFormattedPhone(data.phoneNumber); + setProvider(data.provider); + setStep(STEPS.ENTER_OTP); + const providerName = + data.provider === "kudisms" + ? "KudiSMS" + : data.provider === "termii" + ? "Termii" + : "Twilio"; + toast.success(`OTP sent via ${providerName}`); + } else { + toast.error(data.error || "Failed to send OTP"); + } + } catch (error) { + toast.error("Failed to send OTP. Please try again."); + } finally { + setIsLoading(false); + } + }, [phoneNumber, walletAddress, selectedCountry, name, getAccessToken]); + + const handleOtpSubmit = useCallback(async () => { + if (!otpCode.trim() || otpCode.length !== 6) { + toast.error("Please enter the 6-digit OTP code"); + return; + } + + setIsLoading(true); + + try { + const accessToken = await getAccessToken(); + const response = await fetch("/api/phone/verify-otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + phoneNumber: formattedPhone, + otpCode: otpCode, + }), + }); + + const data = await response.json(); + + if (data.success) { + setStep(STEPS.VERIFIED); + } else { + toast.error(data.error || "Invalid OTP code"); + if (data.attemptsRemaining !== undefined) { + setAttemptsRemaining(data.attemptsRemaining); + } + } + } catch (error) { + toast.error("Failed to verify OTP. Please try again."); + } finally { + setIsLoading(false); + } + }, [otpCode, formattedPhone, getAccessToken]); + + const handleResendOtp = useCallback(async () => { + setIsLoading(true); + try { + const accessToken = await getAccessToken(); + const response = await fetch("/api/phone/send-otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + phoneNumber: formattedPhone, + }), + }); + + const data = await response.json(); + if (data.success) { + setAttemptsRemaining(3); + setOtpCode(""); + toast.success("New OTP sent successfully"); + } else { + toast.error(data.error || "Failed to resend OTP"); + } + } catch (error) { + toast.error("Failed to resend OTP"); + } finally { + setIsLoading(false); + } + }, [formattedPhone, getAccessToken]); + + const handleClose = () => { + onClose(); + // Reset state when modal is closed + setStep(STEPS.ENTER_PHONE); + setPhoneNumber(""); + setFormattedPhone(""); + setOtpCode(""); + setAttemptsRemaining(3); + setIsCountryDropdownOpen(false); + setCountrySearch(""); + }; + + const renderEnterPhone = () => ( + +
+ + + Verify your number to start swapping + +

+ Enter your fullname & phone number to unlock your first swaps on + Noblocks. No extra documents required. +

+
+ +
+
+ + setName(e.target.value)} + placeholder="Enter your fullname" + className="min-h-12 w-full rounded-xl border border-border-input bg-transparent px-4 py-3 text-sm text-neutral-900 transition-all placeholder:text-text-placeholder focus-within:border-gray-400 focus:outline-none disabled:cursor-not-allowed dark:border-white/20 dark:bg-black2 dark:text-white/80 dark:placeholder:text-white/30 dark:focus-within:border-white/40" + /> +
+ +
+ +
+
+ + + {isCountryDropdownOpen && ( +
+
+ setCountrySearch(e.target.value)} + className="w-full rounded-lg border border-border-input bg-transparent px-3 py-2 text-sm text-neutral-900 placeholder:text-text-placeholder focus:border-gray-400 focus:outline-none dark:border-white/20 dark:text-white/80 dark:placeholder:text-white/30 dark:focus:border-white/40" + autoFocus + /> +
+ +
+ {isLoadingCountries ? ( +
+
+ Loading countries... +
+
+ ) : filteredCountries.length > 0 ? ( + filteredCountries.map((country) => ( + + )) + ) : ( +
+
+ No countries found +
+
+ )} +
+
+ )} +
+ + setPhoneNumber(e.target.value)} + placeholder="enter your phone number" + className="min-h-12 w-full rounded-xl border border-border-input bg-transparent py-3 pl-24 pr-4 text-sm text-neutral-900 transition-all placeholder:text-text-placeholder focus-within:border-gray-400 focus:outline-none disabled:cursor-not-allowed dark:border-white/20 dark:bg-black2 dark:text-white/80 dark:placeholder:text-white/30 dark:focus-within:border-white/40" + style={{ + paddingLeft: `${selectedCountry.code.length * 8 + 60}px`, + }} + /> +
+
+
+ + {/* Info message */} +
+ + + + ); + + const renderEnterOtp = () => ( + +
+ setStep(STEPS.ENTER_PHONE)} + /> + + Enter the code we texted you + +

+ We sent a 6-digit code to{" "} + {formattedPhone.replace(/(\+\d+\s+\d{3})[\s\d]+(\d{2})/, "$1**$2")} to + verify your number. +

+
+ + {/* OTP Input */} +
+ +
+ {[...Array(6)].map((_, index) => ( + { + const value = e.target.value.replace(/\D/g, ""); + if (value.length <= 1) { + const newOtp = otpCode.split(""); + newOtp[index] = value; + setOtpCode(newOtp.join("")); + + if (value && index < 5) { + const nextInput = e.target.parentElement?.children[ + index + 1 + ] as HTMLInputElement; + nextInput?.focus(); + } + } + }} + onKeyDown={(e) => { + // Handle backspace to move to previous input + if (e.key === "Backspace" && !otpCode[index] && index > 0) { + const prevInput = (e.target as HTMLInputElement).parentElement + ?.children[index - 1] as HTMLInputElement; + prevInput?.focus(); + } + }} + className="h-[48px] w-[44px] rounded-2xl bg-transparent text-center text-lg font-medium text-neutral-900 transition-all focus-within:border-lavender-600 focus:outline-none dark:bg-surface-overlay dark:text-lavender-600 dark:focus-within:border dark:focus-within:border-lavender-600" + /> + ))} +
+ {attemptsRemaining < 3 && ( + + {attemptsRemaining === 0 + ? "0 attempts remaining, please request a new OTP" + : `${attemptsRemaining} attempts remaining`} + + )} + +
+ Didn't receive a code?{" "} + +
+
+ +
+ + +
+
+ ); + + const renderVerified = () => ( + + + +
+

+ Phone number verification successful! +

+

+ You can now start converting your crypto to fiats at zero fees on + noblocks +

+
+ + +
+ ); + + return ( + + + ); +} diff --git a/app/components/ProfileDrawer.tsx b/app/components/ProfileDrawer.tsx new file mode 100644 index 00000000..d28062f6 --- /dev/null +++ b/app/components/ProfileDrawer.tsx @@ -0,0 +1,434 @@ +"use client"; +import { useState, useEffect, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { toast } from "sonner"; +import { Dialog } from "@headlessui/react"; +import { + ArrowRight03Icon, + ArrowDown01Icon, + Copy01Icon, + StarIcon, + InformationCircleIcon, + FaceIdIcon, + CallingIcon, + WorkAlertIcon, + MapPinpoint01Icon, +} from "hugeicons-react"; +import { usePrivy, useLinkAccount } from "@privy-io/react-auth"; +import { useKYC } from "../context"; +import { KYC_TIERS } from "../context/KYCContext"; +import { formatNumberWithCommas, shortenAddress, classNames } from "../utils"; +import { sidebarAnimation } from "./AnimatedComponents"; +import { PiCheck } from "react-icons/pi"; +import { TbIdBadge } from "react-icons/tb"; +import TransactionLimitModal from "./TransactionLimitModal"; + +interface ProfileDrawerProps { + isOpen: boolean; + onClose: () => void; +} + +export default function ProfileDrawer({ isOpen, onClose }: ProfileDrawerProps) { + const { user } = usePrivy(); + const { + tier, + transactionSummary, + getCurrentLimits, + refreshStatus, + } = useKYC(); + + const [isLimitModalOpen, setIsLimitModalOpen] = useState(false); + const [expandedTiers, setExpandedTiers] = useState>( + {}, + ); + const [isAddressCopied, setIsAddressCopied] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const currentLimits = getCurrentLimits(); + const monthlyLimit = currentLimits.monthly || 0; + const monthlyProgress = + monthlyLimit > 0 + ? (transactionSummary.monthlySpent / monthlyLimit) * 100 + : 0; + + // Refresh KYC status when drawer opens so profile stays in sync with Get started / limit modal + useEffect(() => { + if (isOpen) { + setIsLoading(true); + refreshStatus().finally(() => setIsLoading(false)); + } + }, [isOpen]); // Remove refreshStatus from deps to prevent infinite loop + + const walletAddress = user?.linkedAccounts.find( + (account) => account.type === "smart_wallet", + )?.address; + + const { linkEmail } = useLinkAccount({ + onSuccess: ({ user }) => { + toast.success(`${user.email?.address} linked successfully`); + }, + onError: () => { + toast.error("Error linking account", { + description: "You might have this email linked already", + }); + }, + }); + + const handleCopyAddress = () => { + if (walletAddress) { + navigator.clipboard.writeText(walletAddress); + setIsAddressCopied(true); + toast.success("Address copied to clipboard"); + setTimeout(() => setIsAddressCopied(false), 2000); + } + }; + + const toggleTierExpansion = (tierLevel: number) => { + setExpandedTiers((prev) => ({ + ...prev, + [tierLevel]: !prev[tierLevel], + })); + }; + + // Auto-expand next tier section + useEffect(() => { + if (tier < 1) { + setExpandedTiers((prev) => ({ + ...prev, + [tier + 1]: true, + })); + } + }, [tier]); + + const renderSkeletonLoader = () => ( +
+ {/* Account card skeleton */} +
+
+
+
+
+
+
+
+
+
+ + {/* Current tier skeleton */} +
+
+
+
+
+
+
+
+
+ + {/* Tier sections skeleton */} + {[1, 2].map((i) => ( +
+
+
+ ))} +
+ ); + + const renderTierSection = (tierLevel: number) => { + const tierData = KYC_TIERS[tierLevel]; + const isExpanded = expandedTiers[tierLevel]; + + if (!tierData) return null; + + return ( +
+ + + + {isExpanded && ( + +
+
    + {tierData.requirements.map((req, index) => ( +
  • +
    + {req.includes("number") ? ( + + ) : req.includes("Selfie verification") ? ( + + ) : req.includes("Address verification") ? ( + + ) : req.includes("Business verification") ? ( + + ) : ( + req.includes("ID") && ( + + ) + )} +
    + {req} +
  • + ))} +
+
+

+ Limit +

+

+ {tierData.limits.unlimited ? ( + "Unlimited" + ) : ( + <> + + ${formatNumberWithCommas(tierData.limits.monthly)} + {" "} + / month + + )} +

+
+ {tier == 0 && tierLevel === tier + 1 && ( + + )} +
+
+ )} +
+
+ ); + }; + + return ( + <> + + {isOpen && ( + +
+ {/* Backdrop overlay */} + + + {/* Drawer content */} + +
+ {/* Header with close button */} +
+

+ Profile +

+ +
+ +
+ {isLoading ? ( + renderSkeletonLoader() + ) : ( + <> + {/* Account info card */} +
+

+ Account +

+ + {/* Email Connection */} +
+ +
+ {user?.email ? ( +
+

+ {user.email.address} +

+
+ ) : ( + + )} + + {/* Wallet Address */} +
+

+ {shortenAddress(walletAddress ?? "", 10)} +

+ +
+
+
+
+ + {/* Current Tier Status */} + {tier >= 1 && tier !== undefined && ( +
+ {/* Current Tier Badge */} + +
+ + + Current: {KYC_TIERS[tier]?.name || "Tier 0"} + +
+ + {/* Monthly Limit Progress */} +
+
+ + Monthly limit + + +
+ +
+ $ + {formatNumberWithCommas( + transactionSummary.monthlySpent, + )}{" "} + / $ + {formatNumberWithCommas(currentLimits.monthly)} +
+ + {/* Progress Bar */} +
+
+
+
+ + {/* Upgrade Button */} + {tier < 4 && ( + + )} +
+ )} + + {/* Tier Information */} + + {Object.values(KYC_TIERS) + .filter((tierData) => tierData.level > tier) // Only show tiers above current + .map((tierData) => ( +
+ {renderTierSection(tierData.level)} +
+ ))} + + )} +
+
+ +
+
+ )} +
+ + { + setIsLimitModalOpen(false); + await refreshStatus(); + }} + /> + + ); +} diff --git a/app/components/SettingsDropdown.tsx b/app/components/SettingsDropdown.tsx index 6a7ca7c6..04530f4b 100644 --- a/app/components/SettingsDropdown.tsx +++ b/app/components/SettingsDropdown.tsx @@ -19,6 +19,7 @@ import { Setting07Icon, Wallet01Icon, Key01Icon, + FaceIdIcon, Download01Icon, AccessIcon, } from "hugeicons-react"; @@ -26,6 +27,7 @@ import { toast } from "sonner"; import { useInjectedWallet } from "../context"; import { useWalletDisconnect } from "../hooks/useWalletDisconnect"; import { CopyAddressWarningModal } from "./CopyAddressWarningModal"; +import ProfileDrawer from "./ProfileDrawer"; import { useWallets } from "@privy-io/react-auth"; import { useShouldUseEOA } from "../hooks/useEIP7702Account"; @@ -40,6 +42,7 @@ export const SettingsDropdown = () => { const [isOpen, setIsOpen] = useState(false); const [isAddressCopied, setIsAddressCopied] = useState(false); const [isWarningModalOpen, setIsWarningModalOpen] = useState(false); + const [isProfileDrawerOpen, setIsProfileDrawerOpen] = useState(false); const dropdownRef = useRef(null); useOutsideClick({ @@ -49,10 +52,10 @@ export const SettingsDropdown = () => { // Get embedded wallet (EOA) and smart wallet (SCW) const embeddedWallet = wallets.find( - (wallet) => wallet.walletClientType === "privy" + (wallet) => wallet.walletClientType === "privy", ); const smartWallet = user?.linkedAccounts.find( - (account) => account.type === "smart_wallet" + (account) => account.type === "smart_wallet", ); // Determine active wallet based on migration status @@ -285,6 +288,23 @@ export const SettingsDropdown = () => {

Export wallet

)} +
  • + +
  • + {!isInjectedWallet && (
  • { onClose={() => setIsWarningModalOpen(false)} address={walletAddress ?? ""} /> + + setIsProfileDrawerOpen(false)} + />
  • ); }; diff --git a/app/components/TransactionLimitModal.tsx b/app/components/TransactionLimitModal.tsx new file mode 100644 index 00000000..1f5e5ba1 --- /dev/null +++ b/app/components/TransactionLimitModal.tsx @@ -0,0 +1,255 @@ +"use client"; +import { useEffect, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { + InformationSquareIcon, + Wallet02Icon, + StarIcon, + InformationCircleIcon, +} from "hugeicons-react"; +import { useKYC } from "../context"; +import { KYC_TIERS } from "../context/KYCContext"; +import PhoneVerificationModal from "./PhoneVerificationModal"; +import { primaryBtnClasses, secondaryBtnClasses } from "./Styles"; +import { AnimatedModal, fadeInOut } from "./AnimatedComponents"; +import { formatNumberWithCommas } from "../utils"; +import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react"; +import { KycModal } from "./KycModal"; + +interface TransactionLimitModalProps { + isOpen: boolean; + onClose: () => void; + transactionAmount?: number; +} + +export default function TransactionLimitModal({ + isOpen, + onClose, + transactionAmount = 0, +}: TransactionLimitModalProps) { + const { + tier, + getCurrentLimits, + refreshStatus, + phoneNumber, + transactionSummary, + } = useKYC(); + + const [isPhoneModalOpen, setIsPhoneModalOpen] = useState(false); + const [isKycModalOpen, setIsKycModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + // Refresh KYC status every time modal is opened + useEffect(() => { + if (isOpen) { + setIsLoading(true); + refreshStatus().finally(() => setIsLoading(false)); + } + }, [isOpen, refreshStatus]); + + // Auto-open phone verification modal if tier is less than 1 (unverified) + useEffect(() => { + if ( + isOpen && + !isLoading && + ((tier < 1 && !phoneNumber) || tier === undefined) + ) { + setIsPhoneModalOpen(true); + } + }, [isOpen, isLoading, tier, phoneNumber]); + + const currentLimits = getCurrentLimits(); + const currentTier = KYC_TIERS[tier]; + const nextTier = KYC_TIERS[tier + 1]; + + const handlePhoneVerified = async () => { + setIsPhoneModalOpen(false); + await refreshStatus(); + }; + + const renderLoadingStatus = () => ( + +
    +
    + ); + + const renderMainContent = () => ( + +
    + + + Increase your transaction limit + +

    + Your current monthly limit is{" "} + + ${formatNumberWithCommas(currentLimits.monthly)} + + . Verify your identity to unlock higher limits. +

    +
    + +
    +
    + + + Current: {tier < 1 ? "Tier 0" : currentTier?.name || "Tier 0"} + +
    + + {/* Monthly Limit Progress */} +
    +
    + + Monthly limit + + +
    + + {/* Progress Display */} +
    +
    + ${formatNumberWithCommas(transactionSummary.monthlySpent)} / $ + {formatNumberWithCommas(currentLimits.monthly)} +
    + + {/* Progress Bar */} +
    +
    0 + ? `${Math.min( + (transactionSummary.monthlySpent / + currentLimits.monthly) * + 100, + 100, + )}%` + : "0%", + }} + /> +
    +
    +
    +
    + {/* Info Text */} +
    + +

    + {tier < 1 ? ( + <> + Tier 0 gives you ${formatNumberWithCommas(currentLimits.monthly)} + /month. Verify your ID to unlock $ + {nextTier + ? formatNumberWithCommas(nextTier.limits.monthly) + : "1,000"} + /month and beyond.{" "} + + Learn more. + + + ) : ( + <> + You're currently at {currentTier?.name} with $ + {formatNumberWithCommas(currentLimits.monthly)}/month.{" "} + {nextTier + ? `Upgrade to ${nextTier.name} for ${nextTier.limits.unlimited ? "Unlimited transactions" : `$${formatNumberWithCommas(nextTier.limits.monthly)}/month`}` + : "You have the highest tier available"} + .{" "} + + Learn more. + + + )} +

    +
    + + {/* Action Button */} + {tier < 4 && ( + + )} + + {/* Already at max tier */} + {tier >= 4 && ( + + )} + + ); + + if (!isOpen) return null; + + return ( + <> + + {isOpen && !isPhoneModalOpen && ( + + + )} + + + { + setIsPhoneModalOpen(false); + onClose(); // Close parent modal too + }} + onVerified={handlePhoneVerified} + /> + + + {isKycModalOpen && ( + { + setIsKycModalOpen(false); + onClose(); + }} + > + { + setIsKycModalOpen(value); + if (!value) onClose(); + }} + setIsUserVerified={async () => { + await refreshStatus(); + setIsKycModalOpen(false); + onClose(); + }} + targetTier={tier === 3 ? 4 : tier === 2 ? 3 : 2} + /> + + )} + + + ); +} diff --git a/app/components/index.ts b/app/components/index.ts index e84f5786..30033f56 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -27,6 +27,8 @@ export { KycModal } from "./KycModal"; export { CookieConsent } from "./CookieConsent"; export { NetworkSelectionModal } from "./NetworkSelectionModal"; export { Disclaimer } from "./Disclaimer"; +export { default as PhoneVerificationModal } from "./PhoneVerificationModal"; +export { default as TransactionLimitModal } from "./TransactionLimitModal"; export { TransactionForm } from "../pages/TransactionForm"; export { TransactionPreview } from "../pages/TransactionPreview"; diff --git a/app/components/kyc/DocumentRequirementsModal.tsx b/app/components/kyc/DocumentRequirementsModal.tsx new file mode 100644 index 00000000..e9bc0810 --- /dev/null +++ b/app/components/kyc/DocumentRequirementsModal.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Cancel01Icon, CheckmarkCircle01Icon } from "hugeicons-react"; +import { fadeInOut } from "../AnimatedComponents"; + +const REQUIREMENTS = [ + "Your address must match the address you entered", + "Must be valid and not expired", + "Must clearly show your name, phone and residential address", + "Should not be blurry", +] as const; + +interface DocumentRequirementsModalProps { + isOpen: boolean; + onClose: () => void; + addressDisplay?: string; +} + +export function DocumentRequirementsModal({ + isOpen, + onClose, + addressDisplay, +}: DocumentRequirementsModalProps) { + const list = addressDisplay + ? [ + `Your address must match - ${addressDisplay}`, + ...REQUIREMENTS.slice(1), + ] + : REQUIREMENTS; + + return ( + + {isOpen && ( + + + )} + + ); +} diff --git a/app/components/kyc/index.ts b/app/components/kyc/index.ts new file mode 100644 index 00000000..2641da55 --- /dev/null +++ b/app/components/kyc/index.ts @@ -0,0 +1 @@ +export { DocumentRequirementsModal } from "./DocumentRequirementsModal"; diff --git a/app/context/KYCContext.tsx b/app/context/KYCContext.tsx new file mode 100644 index 00000000..98882b9d --- /dev/null +++ b/app/context/KYCContext.tsx @@ -0,0 +1,249 @@ +"use client"; +import React, { + createContext, + useState, + useContext, + useEffect, + useCallback, + useRef, +} from "react"; +import { useWallets, usePrivy } from "@privy-io/react-auth"; + +// Extend the Window interface for our in-memory fetch guard +declare global { + interface Window { + __KYC_FETCH_GUARDS__?: Record; + } +} + +export interface TransactionLimits { + monthly: number; + unlimited?: boolean; +} + +export type KYCTierLevel = 0 | 1 | 2 | 3 | 4; + +export interface KYCTier { + level: 0 | 1 | 2 | 3 | 4; + name: string; + limits: TransactionLimits; + requirements: string[]; +} + +export const KYC_TIERS: Record = { + 0: { + level: 0, + name: "Tier 0", + limits: { monthly: 0 }, + requirements: [], + }, + 1: { + level: 1, + name: "Tier 1", + limits: { monthly: 100 }, + requirements: ["Phone number"], + }, + 2: { + level: 2, + name: "Tier 2", + limits: { monthly: 15000 }, + requirements: ["Government ID", "Selfie verification"], + }, + 3: { + level: 3, + name: "Tier 3", + limits: { monthly: 50000 }, + requirements: ["Address verification"], + }, + 4: { + level: 4, + name: "Tier 4", + limits: { monthly: 0, unlimited: true }, + requirements: ["Business verification"], + }, +}; + +interface UserTransactionSummary { + dailySpent: number; + monthlySpent: number; + lastTransactionDate: string | null; +} + +interface KYCContextType { + tier: KYCTierLevel; + isPhoneVerified: boolean; + phoneNumber: string | null; + transactionSummary: UserTransactionSummary; + canTransact: (amount: number) => { allowed: boolean; reason?: string }; + getCurrentLimits: () => TransactionLimits; + getRemainingLimits: () => TransactionLimits; + refreshStatus: () => Promise; +} + +const KYCContext = createContext(undefined); + +export function KYCProvider({ children }: { children: React.ReactNode }) { + const { wallets } = useWallets(); + const { getAccessToken } = usePrivy(); + const embeddedWallet = wallets.find( + (wallet) => wallet.walletClientType === "privy", + ); + const walletAddress = embeddedWallet?.address; + + // In-memory guards for fetches per wallet + const fetchGuardsRef = useRef>({}); + const lastFetchTimeRef = useRef(0); + const STALENESS_WINDOW_MS = 30_000; // 30 seconds + const guardKey = walletAddress || "no_wallet"; + + const [tier, setTier] = useState(0); + const [isPhoneVerified, setIsPhoneVerified] = useState(false); + const [phoneNumber, setPhoneNumber] = useState(null); + const [transactionSummary, setTransactionSummary] = + useState({ + dailySpent: 0, + monthlySpent: 0, + lastTransactionDate: null, + }); + + const getCurrentLimits = useCallback((): TransactionLimits => { + return KYC_TIERS[tier].limits; + }, [tier]); + + const getRemainingLimits = useCallback((): TransactionLimits => { + const currentLimits = KYC_TIERS[tier].limits; + const remaining = Math.max( + 0, + currentLimits.monthly - transactionSummary.monthlySpent, + ); + return { monthly: remaining, unlimited: currentLimits.unlimited }; + }, [tier, transactionSummary.monthlySpent]); + + const canTransact = useCallback( + (amount: number): { allowed: boolean; reason?: string } => { + const remaining = getRemainingLimits(); + if (remaining.unlimited) { + return { allowed: true }; + } + if (amount > remaining.monthly) { + return { + allowed: false, + reason: `Transaction amount ($${amount}) exceeds remaining monthly limit ($${remaining.monthly})`, + }; + } + return { allowed: true }; + }, + [getRemainingLimits], + ); + + const fetchTransactionSummary = useCallback(async () => { + if (!walletAddress) return; + const guards = fetchGuardsRef.current; + const key = `${guardKey}_tx`; + if (guards[key] === "fetching") return; + guards[key] = "fetching"; + try { + const accessToken = await getAccessToken(); + if (!accessToken) return; + + const response = await fetch( + `/api/kyc/transaction-summary`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + if (!response.ok) return; + const data = await response.json(); + if (data.success) { + setTransactionSummary({ + dailySpent: data.dailySpent, + monthlySpent: data.monthlySpent, + lastTransactionDate: data.lastTransactionDate, + }); + } + } catch { + // Silently fail — analytics tracked server-side + } finally { + guards[key] = "done"; + } + }, [walletAddress, guardKey, getAccessToken]); + + const fetchKYCStatus = useCallback(async () => { + if (!walletAddress) return; + const guards = fetchGuardsRef.current; + const key = `${guardKey}_kyc`; + if (guards[key] === "fetching") return; + guards[key] = "fetching"; + try { + const accessToken = await getAccessToken(); + if (!accessToken) return; + + const response = await fetch(`/api/kyc/status`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + if (!response.ok) return; + const data = await response.json(); + if (data.success) { + setTier(data.tier); + setIsPhoneVerified(data.isPhoneVerified); + setPhoneNumber(data.phoneNumber); + } + } catch { + // Silently fail — analytics tracked server-side + } finally { + guards[key] = "done"; + } + }, [walletAddress, getAccessToken, guardKey]); + + const refreshStatus = useCallback(async (force = false) => { + // Skip refresh if data is fresh (within staleness window) + const now = Date.now(); + if (!force && now - lastFetchTimeRef.current < STALENESS_WINDOW_MS) return; + + // Reset guards so fetches can run again + const guards = fetchGuardsRef.current; + delete guards[`${guardKey}_kyc`]; + delete guards[`${guardKey}_tx`]; + await Promise.all([fetchKYCStatus(), fetchTransactionSummary()]); + lastFetchTimeRef.current = Date.now(); + }, [fetchKYCStatus, fetchTransactionSummary, guardKey]); + + // Initial load and wallet address change + useEffect(() => { + if (walletAddress) { + // Reset guards when wallet changes + fetchGuardsRef.current = {}; + lastFetchTimeRef.current = 0; + refreshStatus(true); + } + }, [walletAddress, refreshStatus]); + + return ( + + {children} + + ); +} + +export const useKYC = () => { + const context = useContext(KYCContext); + if (context === undefined) { + throw new Error("useKYC must be used within a KYCProvider"); + } + return context; +}; diff --git a/app/context/index.ts b/app/context/index.ts index 99605b92..7f93a1dd 100644 --- a/app/context/index.ts +++ b/app/context/index.ts @@ -16,4 +16,5 @@ export { BlockFestModalProvider, useBlockFestModal, } from "./BlockFestModalContext"; -export { MigrationBannerWrapper } from "./MigrationContext"; \ No newline at end of file +export { KYCProvider, useKYC } from "./KYCContext"; +export { MigrationBannerWrapper } from "./MigrationContext"; diff --git a/app/hooks/useSwapButton.ts b/app/hooks/useSwapButton.ts index 98be7fb7..1383f63a 100644 --- a/app/hooks/useSwapButton.ts +++ b/app/hooks/useSwapButton.ts @@ -102,7 +102,7 @@ export function useSwapButton({ handleSwap: () => void, login: () => void, handleFundWallet: () => void, - setIsKycModalOpen: () => void, + setIsLimitModalOpen: () => void, isUserVerified: boolean, ) => { if (!authenticated && !isInjectedWallet) { @@ -111,8 +111,8 @@ export function useSwapButton({ if (hasInsufficientBalance && !isInjectedWallet && authenticated) { return handleFundWallet; } - if (!isUserVerified && (authenticated || isInjectedWallet)) { - return setIsKycModalOpen; + if (!hasInsufficientBalance && !isUserVerified && (authenticated || isInjectedWallet)) { + return setIsLimitModalOpen; } return handleSwap; }; diff --git a/app/lib/countries.ts b/app/lib/countries.ts new file mode 100644 index 00000000..e0220a4f --- /dev/null +++ b/app/lib/countries.ts @@ -0,0 +1,104 @@ +export interface Country { + code: string; + flag: string; // URL to flag image + name: string; + country: string; +} + +// Cache for countries data +let countriesCache: Country[] | null = null; + +/** + * Fetches country data from REST Countries API + * Returns countries with calling codes, flags, and names + */ +export async function fetchCountries(): Promise { + // Return cached data if available + if (countriesCache) { + return countriesCache; + } + + try { + const response = await fetch( + 'https://restcountries.com/v3.1/all?fields=name,cca2,idd,flag' + ); + + if (!response.ok) { + throw new Error('Failed to fetch countries'); + } + + const data = await response.json(); + + // Transform the API response to our format + const countries: Country[] = data + .filter((country: any) => { + // Only include countries with valid calling codes + return country.idd?.root && country.idd?.suffixes?.length > 0; + }) + .map((country: any) => { + // Get the first calling code (some countries have multiple) + const callingCode = country.idd.root + (country.idd.suffixes[0] || ''); + + return { + code: callingCode, + flag: `https://flagcdn.com/w40/${country.cca2.toLowerCase()}.png`, + name: country.name.common, + country: country.cca2 + }; + }) + .sort((a: Country, b: Country) => a.name.localeCompare(b.name)); // Sort alphabetically + + // Cache the results + countriesCache = countries; + return countries; + } catch (error) { + console.error('Error fetching countries:', error); + + // Fallback to a basic list if API fails + return getDefaultCountries(); + } +} + +/** + * Fallback country list in case the API is unavailable + */ +function getDefaultCountries(): Country[] { + return [ + { code: '+234', flag: 'https://flagcdn.com/w40/ng.png', name: 'Nigeria', country: 'NG' }, + { code: '+254', flag: 'https://flagcdn.com/w40/ke.png', name: 'Kenya', country: 'KE' }, + { code: '+233', flag: 'https://flagcdn.com/w40/gh.png', name: 'Ghana', country: 'GH' }, + { code: '+27', flag: 'https://flagcdn.com/w40/za.png', name: 'South Africa', country: 'ZA' }, + { code: '+1', flag: 'https://flagcdn.com/w40/us.png', name: 'United States', country: 'US' }, + { code: '+44', flag: 'https://flagcdn.com/w40/gb.png', name: 'United Kingdom', country: 'GB' }, + { code: '+33', flag: 'https://flagcdn.com/w40/fr.png', name: 'France', country: 'FR' }, + { code: '+49', flag: 'https://flagcdn.com/w40/de.png', name: 'Germany', country: 'DE' }, + { code: '+81', flag: 'https://flagcdn.com/w40/jp.png', name: 'Japan', country: 'JP' }, + { code: '+86', flag: 'https://flagcdn.com/w40/cn.png', name: 'China', country: 'CN' }, + { code: '+91', flag: 'https://flagcdn.com/w40/in.png', name: 'India', country: 'IN' }, + { code: '+61', flag: 'https://flagcdn.com/w40/au.png', name: 'Australia', country: 'AU' }, + { code: '+55', flag: 'https://flagcdn.com/w40/br.png', name: 'Brazil', country: 'BR' }, + { code: '+52', flag: 'https://flagcdn.com/w40/mx.png', name: 'Mexico', country: 'MX' }, + { code: '+7', flag: 'https://flagcdn.com/w40/ru.png', name: 'Russia', country: 'RU' } + ]; +} + +/** + * Get popular countries that should appear at the top of the list + */ +export function getPopularCountries(): string[] { + return ['NG', 'KE', 'GH', 'ZA', 'US', 'GB', 'CA', 'AU']; +} + +/** + * Search countries by name or calling code + */ +export function searchCountries(countries: Country[], query: string): Country[] { + if (!query.trim()) return countries; + + const searchTerm = query.toLowerCase(); + return countries.filter(country => + country.name.toLowerCase().includes(searchTerm) || + country.code.includes(searchTerm) || + country.country.toLowerCase().includes(searchTerm) + ); +} \ No newline at end of file diff --git a/app/lib/dojah.ts b/app/lib/dojah.ts new file mode 100644 index 00000000..b6991ae4 --- /dev/null +++ b/app/lib/dojah.ts @@ -0,0 +1,71 @@ +/** + * Dojah API client for Tier 3 address verification (utility bill / proof of address). + * Docs: https://docs.dojah.io/docs/document-analysis/utility-bill + */ + +const DOJAH_BASE_URL = + process.env.DOJAH_BASE_URL || "https://api.dojah.io"; +const DOJAH_APP_ID = process.env.DOJAH_APP_ID; +const DOJAH_SECRET_KEY = process.env.DOJAH_SECRET_KEY; + +export interface DojahUtilityBillResponse { + entity?: { + result?: { + status: string; + message?: string; + }; + identity_info?: Record; + address_info?: Record; + provider_name?: string; + bill_issue_date?: string; + amount_paid?: string; + metadata?: { is_recent?: boolean; extraction_date?: string }; + }; +} + +/** + * Submit a utility bill (or similar proof-of-address document) to Dojah for analysis. + * Dojah expects a publicly accessible image URL. + */ +export async function verifyUtilityBill( + imageUrl: string +): Promise { + if (!DOJAH_APP_ID || !DOJAH_SECRET_KEY) { + throw new Error("Dojah is not configured: DOJAH_APP_ID and DOJAH_SECRET_KEY are required"); + } + + const res = await fetch( + `${DOJAH_BASE_URL}/api/v1/document/analysis/utility_bill`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + AppId: DOJAH_APP_ID, + Authorization: DOJAH_SECRET_KEY, + }, + body: JSON.stringify({ + input_type: "url", + input_value: imageUrl, + }), + } + ); + + const data = (await res.json()) as DojahUtilityBillResponse & { message?: string }; + + if (!res.ok) { + const message = data?.message || data?.entity?.result?.message || res.statusText; + throw new Error(message || `Dojah request failed: ${res.status}`); + } + + return data; +} + +/** + * Check if Dojah verification result indicates success. + */ +export function isDojahVerificationSuccess( + data: DojahUtilityBillResponse +): boolean { + const status = data?.entity?.result?.status; + return status === "success"; +} diff --git a/app/lib/phone-verification.ts b/app/lib/phone-verification.ts new file mode 100644 index 00000000..86234c9c --- /dev/null +++ b/app/lib/phone-verification.ts @@ -0,0 +1,196 @@ +import { parsePhoneNumber, CountryCode } from "libphonenumber-js"; +import { randomInt } from "crypto"; +import twilio from "twilio"; + +// Initialize Twilio client +const twilioClient = twilio( + process.env.TWILIO_ACCOUNT_SID!, + process.env.TWILIO_AUTH_TOKEN!, +); + +export interface PhoneVerificationResult { + success: boolean; + message: string; + messageId?: string; + error?: string; +} + +export interface PhoneValidation { + isValid: boolean; + country?: CountryCode; + internationalFormat?: string; + e164Format?: string; // E.164 format without spaces (e.g., +12025550123) + digitsOnly?: string; // Digits only format for KudiSMS (e.g., 2025550123) + isNigerian: boolean; + provider: "kudisms" | "twilio"; +} + +/** + * Validates and parses a phone number + * Returns multiple formats for different use cases: + * - internationalFormat: Display format with spaces (e.g., +1 202 555 0123) + * - e164Format: Twilio-compatible format without spaces (e.g., +12025550123) + * - digitsOnly: KudiSMS-compatible format (e.g., 12025550123) + */ +export function validatePhoneNumber(phoneNumber: string): PhoneValidation { + try { + const parsed = parsePhoneNumber(phoneNumber); + + if (!parsed || !parsed.isValid()) { + return { + isValid: false, + isNigerian: false, + provider: "twilio", + }; + } + + const country = parsed.country as CountryCode; + const isNigerian = country === "NG"; + + return { + isValid: true, + country, + internationalFormat: parsed.formatInternational(), // With spaces for display + e164Format: parsed.format("E.164"), // Without spaces for Twilio + digitsOnly: parsed.number.toString().replace(/\D/g, ""), // Digits only for KudiSMS + isNigerian, + provider: isNigerian ? "kudisms" : "twilio", + }; + } catch (error) { + console.error("Error validating phone number:", error); + return { + isValid: false, + isNigerian: false, + provider: "twilio", + }; + } +} + +/** + * Sends OTP via Kudi SMS for Nigerian numbers + */ +export async function sendKudiSMSOTP( + phoneNumber: string, + code: string, +): Promise { + try { + const response = await fetch("https://my.kudisms.net/api/otp", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + recipients: phoneNumber, + senderID: process.env.KUDISMS_SENDER_ID || "Noblocks", + otp: code, + appnamecode: process.env.KUDISMS_APP_NAME_CODE, + templatecode: process.env.KUDISMS_TEMPLATE_CODE, + token: process.env.KUDISMS_API_KEY, + }), + }); + + const data = await response.json(); + + if (data.status === "success") { + return { + success: true, + message: data.message, + messageId: data.data, + }; + } else { + return { + success: false, + message: "Failed to send OTP via KudiSMS", + error: data.message || "Unknown error", + }; + } + } catch (error) { + console.error("KudiSMS OTP error:", error); + return { + success: false, + message: "Failed to send OTP via KudiSMS", + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} + +/** + * Sends OTP via Twilio Verify for non-Nigerian numbers. + * Twilio generates and sends the code; we do not pass a custom code. + */ +export async function sendTwilioVerifyOTP( + phoneE164: string, +): Promise { + const serviceSid = process.env.TWILIO_VERIFY_SERVICE_SID; + if (!serviceSid) { + console.error("TWILIO_VERIFY_SERVICE_SID is not set"); + return { + success: false, + message: "Twilio Verify is not configured", + error: "Missing TWILIO_VERIFY_SERVICE_SID", + }; + } + + try { + const verification = await twilioClient.verify.v2 + .services(serviceSid) + .verifications.create({ + to: phoneE164, + channel: "sms", + }); + + return { + success: true, + message: "Verification code sent via Twilio Verify", + messageId: verification.sid, + }; + } catch (error: unknown) { + const err = error as { message?: string; code?: number }; + console.error("Twilio Verify send error:", error); + return { + success: false, + message: "Failed to send verification code", + error: err?.message || "Unknown error", + }; + } +} + +/** + * Verifies the code with Twilio Verify (for non-Nigerian numbers). + * Returns true if the verification was approved. + */ +export async function checkTwilioVerifyCode( + phoneE164: string, + code: string, +): Promise<{ success: boolean; error?: string }> { + const serviceSid = process.env.TWILIO_VERIFY_SERVICE_SID; + if (!serviceSid) { + console.error("TWILIO_VERIFY_SERVICE_SID is not set"); + return { success: false, error: "Twilio Verify is not configured" }; + } + + try { + const check = await twilioClient.verify.v2 + .services(serviceSid) + .verificationChecks.create({ + to: phoneE164, + code, + }); + + return { success: check.status === "approved" }; + } catch (error: unknown) { + const err = error as { message?: string }; + console.error("Twilio Verify check error:", error); + return { + success: false, + error: err?.message || "Verification failed", + }; + } +} + +/** + * Generates a 6-digit OTP + */ +export function generateOTP(): string { + return randomInt(100000, 1000000).toString(); +} diff --git a/app/lib/smileID.ts b/app/lib/smileID.ts new file mode 100644 index 00000000..82078671 --- /dev/null +++ b/app/lib/smileID.ts @@ -0,0 +1,93 @@ +let SIDWebAPI: any = null; + +// Dynamically import SmileID only when needed to avoid build-time issues +async function getSIDWebAPI() { + if (!SIDWebAPI) { + const SIDCore = await import("smile-identity-core"); + SIDWebAPI = SIDCore.default.WebApi; + } + return SIDWebAPI; +} + +export type SmileIDIdInfo = { + country: string; + id_type: string; // e.g., "NATIONAL_ID", "PASSPORT", "DRIVERS_LICENSE" + id_number?: string; + first_name?: string; + last_name?: string; + dob?: string; // Date of birth in YYYY-MM-DD format +}; + +// Determines if ID type uses Enhanced KYC (Job Type 5) vs Biometric KYC (Job Type 1) +// Job Type 5: ID number verification only (no document photo needed) - for BVN, NIN, etc. +// Job Type 1: Document verification + face match (requires ID card image) - for Passport, License, etc. +export function getJobTypeForIdType(idType: string): number { + const enhancedKycTypes = ['BVN', 'NIN', 'NIN_SLIP', 'V_NIN']; + return enhancedKycTypes.includes(idType) ? 5 : 1; +} + +export async function submitSmileIDJob({ images, partner_params, walletAddress, id_info }: { + images: any[]; + partner_params: any; + walletAddress: string; + id_info: SmileIDIdInfo; +}) { + // Validate required env vars + const partnerId = process.env.SMILE_IDENTITY_PARTNER_ID; + const callbackUrl = process.env.SMILE_ID_CALLBACK_URL || ""; + const apiKey = process.env.SMILE_IDENTITY_API_KEY; + const serverUrl = process.env.SMILE_IDENTITY_SERVER; + + if (!partnerId || !apiKey || !serverUrl) { + throw new Error("Missing SmileID environment variables"); + } + + // Validate id_info for Job Type 1 + if (!id_info?.country || !id_info?.id_type) { + throw new Error("id_info with country and id_type is required for Biometric KYC"); + } + + const jobType = getJobTypeForIdType(id_info.id_type); + + // Initialize SmileID Web API + const WebApiClass = await getSIDWebAPI(); + const connection = new WebApiClass( + partnerId, + callbackUrl, + apiKey, + serverUrl, + ); + + // Generate unique IDs + const timestamp = Date.now(); + const job_id = `job-${timestamp}-${walletAddress.slice(0, 8)}`; + const user_id = `user-${walletAddress}`; + const smileIdPartnerParams = { + ...partner_params, + user_id, + job_id, + job_type: jobType, + }; + + // Submit to SmileID with id_info for government database verification + const options = { return_job_status: true }; + + try { + const smileIdResult = await connection.submit_job( + smileIdPartnerParams, + images, + id_info, + options, + ); + + return { smileIdResult, job_id, user_id }; + } catch (error: any) { + console.error('SmileID API Error:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + message: error.message, + }); + throw error; + } +} diff --git a/app/lib/supabase.ts b/app/lib/supabase.ts index 211c8377..6931b0d8 100644 --- a/app/lib/supabase.ts +++ b/app/lib/supabase.ts @@ -1,5 +1,6 @@ import { createClient } from '@supabase/supabase-js'; +// Server-only Supabase client (only use in API routes and server components) if (!process.env.SUPABASE_URL) { throw new Error('Missing env.SUPABASE_URL'); } @@ -7,7 +8,6 @@ if (!process.env.SUPABASE_SERVICE_ROLE_KEY) { throw new Error('Missing env.SUPABASE_SERVICE_ROLE_KEY'); } -// Initialize Supabase client with service role key export const supabaseAdmin = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY, @@ -17,4 +17,6 @@ export const supabaseAdmin = createClient( persistSession: false, }, } -); \ No newline at end of file +); + +export const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; \ No newline at end of file diff --git a/app/pages/TransactionForm.tsx b/app/pages/TransactionForm.tsx index e7baad06..a57e5ffe 100644 --- a/app/pages/TransactionForm.tsx +++ b/app/pages/TransactionForm.tsx @@ -15,6 +15,7 @@ import { KycModal, FundWalletForm, AnimatedModal, + TransactionLimitModal, } from "../components"; import { BalanceSkeleton } from "../components/BalanceSkeleton"; import type { TransactionFormProps, Token } from "../types"; @@ -29,7 +30,6 @@ import { } from "../utils"; import { ArrowDown02Icon, NoteEditIcon, Wallet01Icon } from "hugeicons-react"; import { useSwapButton } from "../hooks/useSwapButton"; -import { fetchKYCStatus } from "../api/aggregator"; import { useCNGNRate } from "../hooks/useCNGNRate"; import { useFundWalletHandler } from "../hooks/useFundWalletHandler"; import { useShouldUseEOA } from "../hooks/useEIP7702Account"; @@ -38,6 +38,7 @@ import { useInjectedWallet, useNetwork, useTokens, + useKYC, } from "../context"; /** @@ -69,6 +70,7 @@ export const TransactionForm = ({ const shouldUseEOA = useShouldUseEOA(); const { isInjectedWallet, injectedAddress } = useInjectedWallet(); const { allTokens } = useTokens(); + const { canTransact, refreshStatus, isPhoneVerified, tier } = useKYC(); const embeddedWalletAddress = wallets.find( (wallet) => wallet.walletClientType === "privy", @@ -81,6 +83,8 @@ export const TransactionForm = ({ const [formattedReceivedAmount, setFormattedReceivedAmount] = useState(""); const isFirstRender = useRef(true); const [rateError, setRateError] = useState(null); + const [isLimitModalOpen, setIsLimitModalOpen] = useState(false); + const [blockedTransactionAmount, setBlockedTransactionAmount] = useState(0); const currencies = useMemo( () => @@ -101,6 +105,7 @@ export const TransactionForm = ({ handleSubmit, watch, setValue, + getValues, formState: { errors, isValid, isDirty }, } = formMethods; const { amountSent, amountReceived, token, currency } = watch(); @@ -286,33 +291,13 @@ export const TransactionForm = ({ ); useEffect( - function checkKycStatus() { + function refreshKycStatus() { const walletAddressToCheck = isInjectedWallet ? injectedAddress : embeddedWalletAddress; if (!walletAddressToCheck) return; - const fetchStatus = async () => { - try { - const response = await fetchKYCStatus(walletAddressToCheck); - if (response.data.status === "pending") { - setIsKycModalOpen(true); - } else if (response.data.status === "success") { - setIsUserVerified(true); - } - } catch (error) { - if ( - error instanceof Error && - (error as any).response?.status === 404 - ) { - // silently fail if user is not found/verified - } else { - console.log("error", error); - } - } - }; - - fetchStatus(); + refreshStatus(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [embeddedWalletAddress, injectedAddress, isInjectedWallet], @@ -352,6 +337,19 @@ export const TransactionForm = ({ [amountSent, amountReceived, rate], ); + // Update isUserVerified based on tier changes and current amount + useEffect( + function updateVerificationStatus() { + if (tier > 0 && amountSent) { + const canUserTransact = canTransact(Number(amountSent)).allowed; + setIsUserVerified(canUserTransact); + } else if (tier === 0) { + setIsUserVerified(false); + } + }, + [tier, amountSent, canTransact, setIsUserVerified], + ); + // Register form fields useEffect( function registerFieldsWithValidation() { @@ -474,6 +472,21 @@ export const TransactionForm = ({ const handleSwap = () => { setOrderId(""); + + // Calculate the USD amount for transaction limit checking + const formData = getValues(); + const usdAmount = formData.amountSent || 0; + + // Check transaction limits based on KYC tier + const limitCheck = canTransact(usdAmount); + + if (!limitCheck.allowed) { + setBlockedTransactionAmount(usdAmount); + setIsLimitModalOpen(true); + return; + } + + // If limits are okay, proceed with transaction handleSubmit(onSubmit)(); }; @@ -608,13 +621,8 @@ export const TransactionForm = ({ className="grid gap-4 pb-20 text-sm text-text-body transition-all dark:text-white sm:gap-2" noValidate > -
    -

    - Swap -

    +
    +

    Swap

    - +
    {/* Recipient and memo */} @@ -855,11 +863,21 @@ export const TransactionForm = ({ )} + { + setIsLimitModalOpen(false); + await refreshStatus(); + }} + transactionAmount={blockedTransactionAmount} + /> + {/* Loading and Submit buttons */} {!ready && (