|
@@ -21,11 +21,18 @@ interface PhoneNumber {
|
|
|
id: string;
|
|
id: string;
|
|
|
phone_number: string;
|
|
phone_number: string;
|
|
|
country_code: string;
|
|
country_code: string;
|
|
|
- monthly_price: number;
|
|
|
|
|
|
|
+ country_name: string;
|
|
|
|
|
+ location: string;
|
|
|
|
|
+ price: number;
|
|
|
currency: string;
|
|
currency: string;
|
|
|
is_available: boolean;
|
|
is_available: boolean;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface Country {
|
|
|
|
|
+ country_code: string;
|
|
|
|
|
+ country_name: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export default function IntegrationsRedirect() {
|
|
export default function IntegrationsRedirect() {
|
|
|
const { t } = useTranslation();
|
|
const { t } = useTranslation();
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
@@ -41,10 +48,17 @@ export default function IntegrationsRedirect() {
|
|
|
const [checkingAuth, setCheckingAuth] = useState(true);
|
|
const [checkingAuth, setCheckingAuth] = useState(true);
|
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
|
|
|
|
|
|
- // Phone number selection state
|
|
|
|
|
|
|
+ // Phone number selection state with cascading selectors
|
|
|
|
|
+ const [countries, setCountries] = useState<Country[]>([]);
|
|
|
|
|
+ const [cities, setCities] = useState<string[]>([]);
|
|
|
const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>([]);
|
|
const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>([]);
|
|
|
|
|
+ const [selectedCountry, setSelectedCountry] = useState<string>('');
|
|
|
|
|
+ const [selectedCity, setSelectedCity] = useState<string>('');
|
|
|
const [selectedPhoneNumber, setSelectedPhoneNumber] = useState<string>('');
|
|
const [selectedPhoneNumber, setSelectedPhoneNumber] = useState<string>('');
|
|
|
|
|
+ const [loadingCountries, setLoadingCountries] = useState(false);
|
|
|
|
|
+ const [loadingCities, setLoadingCities] = useState(false);
|
|
|
const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
|
|
const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
|
|
|
|
|
+ const [userCountry, setUserCountry] = useState<string | null>(null);
|
|
|
|
|
|
|
|
// Check authentication status on mount
|
|
// Check authentication status on mount
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -169,19 +183,75 @@ export default function IntegrationsRedirect() {
|
|
|
validateAndSetup();
|
|
validateAndSetup();
|
|
|
}, [searchParams, t]);
|
|
}, [searchParams, t]);
|
|
|
|
|
|
|
|
- // Fetch available phone numbers when user is authenticated
|
|
|
|
|
|
|
+ // Fetch available countries when user is authenticated
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- const fetchPhoneNumbers = async () => {
|
|
|
|
|
|
|
+ const fetchCountries = async () => {
|
|
|
if (!isAuthenticated || !pendingInstall) return;
|
|
if (!isAuthenticated || !pendingInstall) return;
|
|
|
|
|
|
|
|
- setLoadingPhoneNumbers(true);
|
|
|
|
|
|
|
+ setLoadingCountries(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) return;
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+ const response = await fetch(`${API_URL}/api/phone-numbers?group_by=countries`, {
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ setCountries(data.countries || []);
|
|
|
|
|
+ // Auto-select user's country if available
|
|
|
|
|
+ if (data.user_country) {
|
|
|
|
|
+ setUserCountry(data.user_country);
|
|
|
|
|
+ // Check if user's country is in the available countries list
|
|
|
|
|
+ const hasUserCountry = (data.countries || []).some(
|
|
|
|
|
+ (c: Country) => c.country_code === data.user_country
|
|
|
|
|
+ );
|
|
|
|
|
+ if (hasUserCountry) {
|
|
|
|
|
+ setSelectedCountry(data.user_country);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('Failed to fetch countries:', response.status);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Error fetching countries:', err);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoadingCountries(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ fetchCountries();
|
|
|
|
|
+ }, [isAuthenticated, pendingInstall]);
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch cities when country is selected
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const fetchCities = async () => {
|
|
|
|
|
+ if (!selectedCountry) {
|
|
|
|
|
+ setCities([]);
|
|
|
|
|
+ setSelectedCity('');
|
|
|
|
|
+ setPhoneNumbers([]);
|
|
|
|
|
+ setSelectedPhoneNumber('');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setLoadingCities(true);
|
|
|
|
|
+ setCities([]);
|
|
|
|
|
+ setSelectedCity('');
|
|
|
|
|
+ setPhoneNumbers([]);
|
|
|
|
|
+ setSelectedPhoneNumber('');
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const sessionData = localStorage.getItem('session_data');
|
|
const sessionData = localStorage.getItem('session_data');
|
|
|
if (!sessionData) return;
|
|
if (!sessionData) return;
|
|
|
|
|
|
|
|
const session = JSON.parse(sessionData);
|
|
const session = JSON.parse(sessionData);
|
|
|
- // Fetch all available phone numbers (no country filter for store integration)
|
|
|
|
|
- const response = await fetch(`${API_URL}/api/phone-numbers?available=true&all_countries=true`, {
|
|
|
|
|
|
|
+ const response = await fetch(`${API_URL}/api/phone-numbers?group_by=cities&country=${selectedCountry}`, {
|
|
|
method: 'GET',
|
|
method: 'GET',
|
|
|
headers: {
|
|
headers: {
|
|
|
'Authorization': `Bearer ${session.session.access_token}`,
|
|
'Authorization': `Bearer ${session.session.access_token}`,
|
|
@@ -189,6 +259,51 @@ export default function IntegrationsRedirect() {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ setCities(data.cities || []);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('Failed to fetch cities:', response.status);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Error fetching cities:', err);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoadingCities(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ fetchCities();
|
|
|
|
|
+ }, [selectedCountry]);
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch phone numbers when city is selected
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const fetchPhoneNumbers = async () => {
|
|
|
|
|
+ if (!selectedCountry || !selectedCity) {
|
|
|
|
|
+ setPhoneNumbers([]);
|
|
|
|
|
+ setSelectedPhoneNumber('');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setLoadingPhoneNumbers(true);
|
|
|
|
|
+ setPhoneNumbers([]);
|
|
|
|
|
+ setSelectedPhoneNumber('');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) return;
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+ const response = await fetch(
|
|
|
|
|
+ `${API_URL}/api/phone-numbers?available=true&country=${selectedCountry}&city=${encodeURIComponent(selectedCity)}`,
|
|
|
|
|
+ {
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
if (response.ok) {
|
|
if (response.ok) {
|
|
|
const data = await response.json();
|
|
const data = await response.json();
|
|
|
setPhoneNumbers(data.phone_numbers || []);
|
|
setPhoneNumbers(data.phone_numbers || []);
|
|
@@ -203,7 +318,7 @@ export default function IntegrationsRedirect() {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
fetchPhoneNumbers();
|
|
fetchPhoneNumbers();
|
|
|
- }, [isAuthenticated, pendingInstall]);
|
|
|
|
|
|
|
+ }, [selectedCountry, selectedCity]);
|
|
|
|
|
|
|
|
// Handle auth state changes
|
|
// Handle auth state changes
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -433,61 +548,143 @@ export default function IntegrationsRedirect() {
|
|
|
</DialogDescription>
|
|
</DialogDescription>
|
|
|
</DialogHeader>
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
- {/* Phone Number Selection */}
|
|
|
|
|
- <div className="mt-4 space-y-3">
|
|
|
|
|
|
|
+ {/* Phone Number Selection - Cascading: Country → City → Phone */}
|
|
|
|
|
+ <div className="mt-4 space-y-4">
|
|
|
|
|
+ {/* Country Selector */}
|
|
|
<div className="space-y-2">
|
|
<div className="space-y-2">
|
|
|
<Label className="text-slate-300 flex items-center gap-2">
|
|
<Label className="text-slate-300 flex items-center gap-2">
|
|
|
- <Phone className="w-4 h-4" />
|
|
|
|
|
- {t('integrations.oauth.selectPhoneNumber', 'Select Phone Number')}
|
|
|
|
|
|
|
+ {t('integrations.oauth.selectCountry', 'Country')}
|
|
|
<span className="text-red-400">*</span>
|
|
<span className="text-red-400">*</span>
|
|
|
</Label>
|
|
</Label>
|
|
|
- {loadingPhoneNumbers ? (
|
|
|
|
|
|
|
+ {loadingCountries ? (
|
|
|
<div className="flex items-center justify-center py-3">
|
|
<div className="flex items-center justify-center py-3">
|
|
|
<Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
|
|
<Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
|
|
|
<span className="ml-2 text-sm text-slate-400">
|
|
<span className="ml-2 text-sm text-slate-400">
|
|
|
- {t('integrations.oauth.loadingPhoneNumbers', 'Loading available phone numbers...')}
|
|
|
|
|
|
|
+ {t('integrations.oauth.loadingCountries', 'Loading countries...')}
|
|
|
</span>
|
|
</span>
|
|
|
</div>
|
|
</div>
|
|
|
- ) : phoneNumbers.length > 0 ? (
|
|
|
|
|
- <>
|
|
|
|
|
- <Select value={selectedPhoneNumber} onValueChange={setSelectedPhoneNumber}>
|
|
|
|
|
- <SelectTrigger className={`bg-slate-700 border-slate-600 text-white ${!selectedPhoneNumber ? 'border-red-500/50' : ''}`}>
|
|
|
|
|
- <SelectValue placeholder={t('integrations.oauth.selectPhonePlaceholder', 'Select a phone number')} />
|
|
|
|
|
- </SelectTrigger>
|
|
|
|
|
- <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
|
|
- {phoneNumbers.map((phone) => (
|
|
|
|
|
- <SelectItem key={phone.id} value={phone.id} className="text-white hover:bg-slate-600">
|
|
|
|
|
- <div className="flex items-center justify-between w-full">
|
|
|
|
|
- <span>{phone.phone_number}</span>
|
|
|
|
|
- <span className="ml-2 text-sm text-slate-400">
|
|
|
|
|
- {phone.monthly_price > 0 ? `${phone.monthly_price} ${phone.currency}/mo` : t('common.free', 'Free')}
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </SelectItem>
|
|
|
|
|
- ))}
|
|
|
|
|
- </SelectContent>
|
|
|
|
|
- </Select>
|
|
|
|
|
- {!selectedPhoneNumber && (
|
|
|
|
|
- <p className="text-xs text-red-400">
|
|
|
|
|
- {t('integrations.oauth.phoneNumberRequired', 'Please select a phone number to continue.')}
|
|
|
|
|
- </p>
|
|
|
|
|
- )}
|
|
|
|
|
- </>
|
|
|
|
|
|
|
+ ) : countries.length > 0 ? (
|
|
|
|
|
+ <Select value={selectedCountry} onValueChange={setSelectedCountry}>
|
|
|
|
|
+ <SelectTrigger className={`bg-slate-700 border-slate-600 text-white ${!selectedCountry ? 'border-red-500/50' : ''}`}>
|
|
|
|
|
+ <SelectValue placeholder={t('integrations.oauth.selectCountryPlaceholder', 'Select a country')} />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
|
|
+ {countries.map((country) => (
|
|
|
|
|
+ <SelectItem key={country.country_code} value={country.country_code} className="text-white hover:bg-slate-600">
|
|
|
|
|
+ {country.country_name}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
) : (
|
|
) : (
|
|
|
<div className="bg-red-500/10 border border-red-500/30 rounded-md p-3">
|
|
<div className="bg-red-500/10 border border-red-500/30 rounded-md p-3">
|
|
|
<p className="text-sm text-red-400">
|
|
<p className="text-sm text-red-400">
|
|
|
- {t('integrations.oauth.noPhoneNumbersError', 'No phone numbers available. Please add a phone number first before connecting your store.')}
|
|
|
|
|
|
|
+ {t('integrations.oauth.noCountriesError', 'No countries with available phone numbers.')}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* City Selector - shown after country selection */}
|
|
|
|
|
+ {selectedCountry && (
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label className="text-slate-300 flex items-center gap-2">
|
|
|
|
|
+ {t('integrations.oauth.selectCity', 'City')}
|
|
|
|
|
+ <span className="text-red-400">*</span>
|
|
|
|
|
+ </Label>
|
|
|
|
|
+ {loadingCities ? (
|
|
|
|
|
+ <div className="flex items-center justify-center py-3">
|
|
|
|
|
+ <Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
|
|
|
|
|
+ <span className="ml-2 text-sm text-slate-400">
|
|
|
|
|
+ {t('integrations.oauth.loadingCities', 'Loading cities...')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : cities.length > 0 ? (
|
|
|
|
|
+ <Select value={selectedCity} onValueChange={setSelectedCity}>
|
|
|
|
|
+ <SelectTrigger className={`bg-slate-700 border-slate-600 text-white ${!selectedCity ? 'border-red-500/50' : ''}`}>
|
|
|
|
|
+ <SelectValue placeholder={t('integrations.oauth.selectCityPlaceholder', 'Select a city')} />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
|
|
+ {cities.map((city) => (
|
|
|
|
|
+ <SelectItem key={city} value={city} className="text-white hover:bg-slate-600">
|
|
|
|
|
+ {city}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="bg-yellow-500/10 border border-yellow-500/30 rounded-md p-3">
|
|
|
|
|
+ <p className="text-sm text-yellow-400">
|
|
|
|
|
+ {t('integrations.oauth.noCitiesError', 'No cities with available phone numbers in this country.')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Phone Number Selector - shown after city selection */}
|
|
|
|
|
+ {selectedCity && (
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Label className="text-slate-300 flex items-center gap-2">
|
|
|
|
|
+ <Phone className="w-4 h-4" />
|
|
|
|
|
+ {t('integrations.oauth.selectPhoneNumber', 'Phone Number')}
|
|
|
|
|
+ <span className="text-red-400">*</span>
|
|
|
|
|
+ </Label>
|
|
|
|
|
+ {loadingPhoneNumbers ? (
|
|
|
|
|
+ <div className="flex items-center justify-center py-3">
|
|
|
|
|
+ <Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
|
|
|
|
|
+ <span className="ml-2 text-sm text-slate-400">
|
|
|
|
|
+ {t('integrations.oauth.loadingPhoneNumbers', 'Loading available phone numbers...')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : phoneNumbers.length > 0 ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Select value={selectedPhoneNumber} onValueChange={setSelectedPhoneNumber}>
|
|
|
|
|
+ <SelectTrigger className={`bg-slate-700 border-slate-600 text-white ${!selectedPhoneNumber ? 'border-red-500/50' : ''}`}>
|
|
|
|
|
+ <SelectValue placeholder={t('integrations.oauth.selectPhonePlaceholder', 'Select a phone number')} />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
|
|
+ {phoneNumbers.map((phone) => (
|
|
|
|
|
+ <SelectItem key={phone.id} value={phone.id} className="text-white hover:bg-slate-600">
|
|
|
|
|
+ <div className="flex items-center justify-between w-full">
|
|
|
|
|
+ <span>{phone.phone_number}</span>
|
|
|
|
|
+ <span className="ml-2 text-sm text-slate-400">
|
|
|
|
|
+ {phone.price > 0 ? `${phone.price}/mo` : t('common.free', 'Free')}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ {!selectedPhoneNumber && (
|
|
|
|
|
+ <p className="text-xs text-red-400">
|
|
|
|
|
+ {t('integrations.oauth.phoneNumberRequired', 'Please select a phone number to continue.')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="bg-red-500/10 border border-red-500/30 rounded-md p-3">
|
|
|
|
|
+ <p className="text-sm text-red-400">
|
|
|
|
|
+ {t('integrations.oauth.noPhoneNumbersError', 'No phone numbers available in this city.')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Show message when not all selections made */}
|
|
|
|
|
+ {!selectedPhoneNumber && countries.length > 0 && (
|
|
|
|
|
+ <p className="text-xs text-slate-400">
|
|
|
|
|
+ {t('integrations.oauth.selectionRequired', 'Please select a country, city, and phone number to continue.')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="space-y-3 mt-4">
|
|
<div className="space-y-3 mt-4">
|
|
|
<Button
|
|
<Button
|
|
|
className="w-full bg-cyan-500 hover:bg-cyan-600 text-white disabled:opacity-50 disabled:cursor-not-allowed"
|
|
className="w-full bg-cyan-500 hover:bg-cyan-600 text-white disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
onClick={completeInstallation}
|
|
onClick={completeInstallation}
|
|
|
- disabled={completing || loadingPhoneNumbers || !selectedPhoneNumber || phoneNumbers.length === 0}
|
|
|
|
|
|
|
+ disabled={completing || loadingCountries || loadingCities || loadingPhoneNumbers || !selectedPhoneNumber || countries.length === 0}
|
|
|
>
|
|
>
|
|
|
{completing ? (
|
|
{completing ? (
|
|
|
<>
|
|
<>
|