463 lines
15 KiB
TypeScript
463 lines
15 KiB
TypeScript
import React, { useState, useRef, useEffect } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
Alert,
|
|
ActivityIndicator,
|
|
} from "react-native";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Input } from "~/components/ui/input";
|
|
import { UploadCloud } from "lucide-react-native";
|
|
import Dropdown, { type DropdownOption } from "~/components/ui/dropdown";
|
|
import ModalToast from "~/components/ui/toast";
|
|
import BackButton from "~/components/ui/backButton";
|
|
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
|
|
import AuthService from "~/lib/services/authServices";
|
|
import { uploadKycDocument } from "~/lib/services/kycDocumentService";
|
|
import * as ImagePicker from "expo-image-picker";
|
|
|
|
export default function KycScreen() {
|
|
const { user, refreshProfile } = useAuthWithProfile();
|
|
const [activeTab, setActiveTab] = useState<"personal" | "business">(
|
|
"personal"
|
|
);
|
|
const [fanNumber, setFanNumber] = useState("");
|
|
const [tin, setTin] = useState("");
|
|
const [businessType, setBusinessType] = useState("");
|
|
const [nationalIdUri, setNationalIdUri] = useState<string | null>(null);
|
|
const [nationalIdLabel, setNationalIdLabel] = useState("");
|
|
const [businessLicenseUri, setBusinessLicenseUri] = useState<string | null>(
|
|
null
|
|
);
|
|
const [businessLicenseLabel, setBusinessLicenseLabel] = useState("");
|
|
const [pickingNationalId, setPickingNationalId] = useState(false);
|
|
const [pickingBusinessLicense, setPickingBusinessLicense] = useState(false);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const [toastVisible, setToastVisible] = useState(false);
|
|
const [toastTitle, setToastTitle] = useState("");
|
|
const [toastDescription, setToastDescription] = useState<string | undefined>(
|
|
undefined
|
|
);
|
|
const [toastVariant, setToastVariant] = useState<
|
|
"success" | "error" | "warning" | "info"
|
|
>("info");
|
|
const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const businessTypeOptions: DropdownOption[] = [
|
|
{ label: "Retail & E-commerce", value: "Retail & E-commerce" },
|
|
{ label: "Food & Beverage", value: "Food & Beverage" },
|
|
{
|
|
label: "Transportation & Logistics",
|
|
value: "Transportation & Logistics",
|
|
},
|
|
{ label: "Finance & Fintech", value: "Finance & Fintech" },
|
|
{
|
|
label: "Healthcare & Pharmacy",
|
|
value: "Healthcare & Pharmacy",
|
|
},
|
|
{ label: "Education & Training", value: "Education & Training" },
|
|
{
|
|
label: "Construction & Real Estate",
|
|
value: "Construction & Real Estate",
|
|
},
|
|
{
|
|
label: "Technology & Software",
|
|
value: "Technology & Software",
|
|
},
|
|
{ label: "Entertainment & Events", value: "Entertainment & Events" },
|
|
{ label: "Agriculture & Farming", value: "Agriculture & Farming" },
|
|
{
|
|
label: "Freelancer / Sole Proprietor",
|
|
value: "Freelancer / Sole Proprietor",
|
|
},
|
|
{ label: "Other", value: "Other" },
|
|
];
|
|
|
|
const showToast = (
|
|
title: string,
|
|
description?: string,
|
|
variant: "success" | "error" | "warning" | "info" = "info"
|
|
) => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
|
|
setToastTitle(title);
|
|
setToastDescription(description);
|
|
setToastVariant(variant);
|
|
setToastVisible(true);
|
|
|
|
toastTimeoutRef.current = setTimeout(() => {
|
|
setToastVisible(false);
|
|
toastTimeoutRef.current = null;
|
|
}, 2500);
|
|
};
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const handlePrimary = async () => {
|
|
if (!user?.uid) {
|
|
showToast("Error", "You must be logged in to save KYC.", "error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSubmitting(true);
|
|
|
|
const updateData: any = {};
|
|
|
|
if (activeTab === "personal") {
|
|
if (!fanNumber.trim()) {
|
|
showToast("Error", "Please enter your FAN Number.", "error");
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
updateData.fanNumber = fanNumber.trim();
|
|
|
|
if (nationalIdUri) {
|
|
try {
|
|
const url = await uploadKycDocument(
|
|
user.uid,
|
|
nationalIdUri,
|
|
"personal-id"
|
|
);
|
|
updateData.nationalIdUrl = url;
|
|
} catch (e) {
|
|
console.error("[KYC] Failed to upload national ID", e);
|
|
showToast("Error", "Failed to upload National ID.", "error");
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
if (!tin.trim()) {
|
|
showToast("Error", "Please enter your EIN / TIN.", "error");
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
if (!businessType.trim()) {
|
|
showToast("Error", "Please enter your business type.", "error");
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
updateData.tin = tin.trim();
|
|
updateData.businessType = businessType.trim();
|
|
|
|
if (businessLicenseUri) {
|
|
try {
|
|
const url = await uploadKycDocument(
|
|
user.uid,
|
|
businessLicenseUri,
|
|
"business-license"
|
|
);
|
|
updateData.businessLicenseUrl = url;
|
|
} catch (e) {
|
|
console.error("[KYC] Failed to upload business license", e);
|
|
showToast("Error", "Failed to upload Business License.", "error");
|
|
setSubmitting(false);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = await AuthService.updateUserProfile(user.uid, updateData);
|
|
if (!result.success) {
|
|
console.error("[KYC] updateUserProfile failed", result.error);
|
|
showToast("Error", result.error || "Failed to save KYC.", "error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await refreshProfile();
|
|
} catch (e) {
|
|
console.warn("[KYC] Failed to refresh profile after KYC save", e);
|
|
}
|
|
|
|
// Successful save: clear all KYC fields
|
|
setFanNumber("");
|
|
setTin("");
|
|
setBusinessType("");
|
|
setNationalIdUri(null);
|
|
setNationalIdLabel("");
|
|
setBusinessLicenseUri(null);
|
|
setBusinessLicenseLabel("");
|
|
|
|
showToast("Success", "KYC information saved.", "success");
|
|
} catch (e) {
|
|
console.error("[KYC] Unexpected error while saving KYC", e);
|
|
showToast("Error", "Unexpected error while saving KYC.", "error");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handlePickNationalId = async () => {
|
|
if (pickingNationalId) return;
|
|
|
|
try {
|
|
setPickingNationalId(true);
|
|
|
|
const permissionResult =
|
|
await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
if (!permissionResult.granted) {
|
|
Alert.alert(
|
|
"Permission Required",
|
|
"Please allow access to your photo library to upload your ID."
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
allowsEditing: true,
|
|
quality: 0.8,
|
|
});
|
|
|
|
if (!result.canceled && result.assets[0]) {
|
|
const uri = result.assets[0].uri;
|
|
setNationalIdUri(uri);
|
|
const name = uri.split("/").pop() || "Selected file";
|
|
setNationalIdLabel(name);
|
|
}
|
|
} catch (e) {
|
|
console.error("[KYC] Error while selecting National ID", e);
|
|
showToast("Error", "Failed to select National ID.", "error");
|
|
} finally {
|
|
setPickingNationalId(false);
|
|
}
|
|
};
|
|
|
|
const handlePickBusinessLicense = async () => {
|
|
if (pickingBusinessLicense) return;
|
|
|
|
try {
|
|
setPickingBusinessLicense(true);
|
|
|
|
const permissionResult =
|
|
await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
if (!permissionResult.granted) {
|
|
Alert.alert(
|
|
"Permission Required",
|
|
"Please allow access to your photo library to upload your Business License."
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
allowsEditing: true,
|
|
quality: 0.8,
|
|
});
|
|
|
|
if (!result.canceled && result.assets[0]) {
|
|
const uri = result.assets[0].uri;
|
|
setBusinessLicenseUri(uri);
|
|
const name = uri.split("/").pop() || "Selected file";
|
|
setBusinessLicenseLabel(name);
|
|
}
|
|
} catch (e) {
|
|
console.error("[KYC] Error while selecting Business License", e);
|
|
showToast("Error", "Failed to select Business License.", "error");
|
|
} finally {
|
|
setPickingBusinessLicense(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<BackButton />
|
|
|
|
<ScrollView
|
|
className="flex-1 bg-white"
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<View className="px-5 pb-8">
|
|
<Text className="text-3xl font-dmsans-bold text-gray-900 mb-2">
|
|
Information
|
|
</Text>
|
|
<Text className="text-lg font-dmsans-bold text-[#0F7B4A] mb-1">
|
|
KYC
|
|
</Text>
|
|
<Text className="text-base font-dmsans text-gray-500 mb-5">
|
|
Fill out the information below to add more limits to your account
|
|
</Text>
|
|
{/* tab */}
|
|
<View className="flex-row mb-5 bg-[#F3F4F6] rounded-full p-1">
|
|
<TouchableOpacity
|
|
onPress={() => setActiveTab("personal")}
|
|
className={`flex-1 items-center py-2 rounded-full ${
|
|
activeTab === "personal" ? "bg-white" : "bg-transparent"
|
|
}`}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text
|
|
className={`text-base font-dmsans-medium ${
|
|
activeTab === "personal" ? "text-[#0F7B4A]" : "text-gray-500"
|
|
}`}
|
|
>
|
|
Personal
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => setActiveTab("business")}
|
|
className={`flex-1 items-center py-2 rounded-full ${
|
|
activeTab === "business" ? "bg-white" : "bg-transparent"
|
|
}`}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text
|
|
className={`text-base font-dmsans-medium ${
|
|
activeTab === "business" ? "text-[#0F7B4A]" : "text-gray-500"
|
|
}`}
|
|
>
|
|
Business
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{activeTab === "personal" ? (
|
|
<View className="mt-2">
|
|
<Text className="text-base font-dmsans text-black mb-2">
|
|
FAN Number
|
|
</Text>
|
|
<Input
|
|
placeholderText="FAN Number"
|
|
value={fanNumber}
|
|
onChangeText={setFanNumber}
|
|
containerClassName="w-full mb-4"
|
|
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
|
|
placeholderColor="#9CA3AF"
|
|
textClassName="text-[#111827] text-sm"
|
|
/>
|
|
|
|
<Text className="text-base font-dmsans text-black mb-2">
|
|
National ID Upload
|
|
</Text>
|
|
<TouchableOpacity
|
|
activeOpacity={0.8}
|
|
onPress={handlePickNationalId}
|
|
disabled={pickingNationalId}
|
|
>
|
|
<Input
|
|
placeholderText={
|
|
pickingNationalId
|
|
? "Opening picker..."
|
|
: "Upload National ID"
|
|
}
|
|
value={nationalIdLabel}
|
|
containerClassName="w-full"
|
|
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
|
|
placeholderColor="#9CA3AF"
|
|
textClassName="text-[#111827] text-sm"
|
|
editable={false}
|
|
rightIcon={
|
|
pickingNationalId ? (
|
|
<ActivityIndicator size="small" color="#111827" />
|
|
) : (
|
|
<UploadCloud size={18} color="#111827" />
|
|
)
|
|
}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<View className="mt-2">
|
|
<Text className="text-base font-dmsans text-black mb-2">
|
|
EIN / TIN
|
|
</Text>
|
|
<Input
|
|
placeholderText="EIN / TIN"
|
|
value={tin}
|
|
onChangeText={setTin}
|
|
containerClassName="w-full mb-4"
|
|
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
|
|
placeholderColor="#9CA3AF"
|
|
textClassName="text-[#111827] text-sm"
|
|
/>
|
|
|
|
<Text className="text-base font-dmsans text-black mb-2">
|
|
Type of Business
|
|
</Text>
|
|
<View className="w-full mb-4">
|
|
<Dropdown
|
|
value={businessType || null}
|
|
options={businessTypeOptions}
|
|
onSelect={(value) => setBusinessType(value)}
|
|
placeholder="Select type of business"
|
|
/>
|
|
</View>
|
|
|
|
<Text className="text-base font-dmsans text-black mb-2">
|
|
Business License
|
|
</Text>
|
|
<TouchableOpacity
|
|
activeOpacity={0.8}
|
|
onPress={handlePickBusinessLicense}
|
|
disabled={pickingBusinessLicense}
|
|
>
|
|
<Input
|
|
placeholderText={
|
|
pickingBusinessLicense
|
|
? "Opening picker..."
|
|
: "In-cooperation Document"
|
|
}
|
|
value={businessLicenseLabel}
|
|
containerClassName="w-full"
|
|
borderClassName="border-[#E5E7EB] bg-white rounded-[4px]"
|
|
placeholderColor="#9CA3AF"
|
|
textClassName="text-[#111827] text-sm"
|
|
editable={false}
|
|
rightIcon={
|
|
pickingBusinessLicense ? (
|
|
<ActivityIndicator size="small" color="#111827" />
|
|
) : (
|
|
<UploadCloud size={18} color="#111827" />
|
|
)
|
|
}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
|
|
<View className="w-full px-5 pb-8 bg-white">
|
|
<Button
|
|
className="h-13 rounded-full bg-[#0F7B4A]"
|
|
onPress={handlePrimary}
|
|
disabled={submitting}
|
|
>
|
|
<Text className="text-white font-dmsans-bold text-base">
|
|
{submitting
|
|
? "Saving..."
|
|
: activeTab === "personal"
|
|
? "Done"
|
|
: "Add"}
|
|
</Text>
|
|
</Button>
|
|
</View>
|
|
|
|
<ModalToast
|
|
visible={toastVisible}
|
|
title={toastTitle}
|
|
description={toastDescription}
|
|
variant={toastVariant}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|