2024-12-24 22:19:18 +07:00

585 lines
21 KiB
JavaScript

import React, { useState, useRef, useEffect } from 'react'
import { ServerDownAnimation } from '../../../../assets/images';
import Select from 'react-select'
const BASE_URL = process.env.REACT_APP_BASE_URL
const API_KEY = process.env.REACT_APP_API_KEY
const Verify = ({ onVerify, generateId }) => {
const [otpCode, setOtpCode] = useState('');
const [generateCode, setGenerateCode] = useState(generateId);
const [error, setError] = useState('');
useEffect(() => {
setGenerateCode(generateId);
}, [generateId]);
const handleClick = async () => {
// Validation logic
if (!otpCode) {
setError("OTP Code is required.");
return;
}
const requestData = {
id: generateId,
otp: otpCode
};
fetch(`${BASE_URL}/wa/otp-verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.status_code === 201) {
setError(''); // Clear any existing error message
alert('OTP verification successful!'); // Show success alert
onVerify(); // Trigger verify callback
} else {
setError(data.details.message || "Verification failed");
}
})
.catch(error => {
console.error('Verification failed:', error);
setError("Failed to verify OTP");
});
};
const generateTimestampId = () => {
const timestamp = new Date().getTime().toString(16); // Convert timestamp to hex
const randomPart = Math.random().toString(16).substr(2, 8);
return `${timestamp}-${randomPart}-${randomPart}-${randomPart}`;
};
return (
<div style={styles.section}>
{/* Results Data */}
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<input
type="text"
id="generateCode"
className="form-control"
placeholder="Generated Code"
value={`${generateTimestampId()}-${generateCode}`}
onChange={(e) => setGenerateCode(e.target.value)}
readOnly
/>
</div>
</div>
</div>
{/* OTP Code */}
<div className="form-group row align-items-center mt-3">
<div className="col-md-6">
<input
type="text"
id="otpCode"
className="form-control"
placeholder="OTP Code"
value={otpCode}
onChange={(e) => {
const value = e.target.value;
if (/^\d{0,6}$/.test(value)) {
setOtpCode(value);
}
}}
maxLength={6}
/>
{error && <small className="text-danger">{error}</small>}
</div>
</div>
{/* Verify Button */}
<div style={styles.submitButton}>
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Verify</p>
</button>
</div>
</div>
);
}
const Preview = ({ otpLength }) => {
// Generate OTP when otpLength changes
const generateOTP = (length) => {
let otp = '';
for (let i = 0; i < length; i++) {
otp += Math.floor(Math.random() * 10);
}
return otp;
};
// State to store the generated OTP
const [inputValue, setInputValue] = useState('');
// Update OTP whenever otpLength changes
useEffect(() => {
setInputValue(generateOTP(parseInt(otpLength))); // Re-generate OTP on otpLength change
}, [otpLength]); // Dependency array with otpLength
const handleCopy = () => {
navigator.clipboard.writeText(inputValue);
console.log('Copied');
};
return (
<div className="form-group row align-items-center mt-4">
<div className="col-10">
<div style={styles.selectWrapper}>
<input
type="text"
id="inputValue"
className="form-control"
value={`${inputValue} is your verification code. For your security, do not share this code.`}
onChange={(e) => setInputValue(e.target.value)}
readOnly
/>
</div>
</div>
<div className="col-2 d-flex">
<div style={styles.selectWrapper}>
<button className="btn btn-primary" onClick={handleCopy}>
Copy
</button>
</div>
</div>
</div>
);
};
const Auth = () => {
const [applicationId, setApplicationId] = useState('');
const [expiryId, setExpiryId] = useState(0);
const [otpId, setOtpId] = useState('');
const [phoneId, setPhoneId] = useState('');
const [templateId, setTemplateId] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [applicationIds, setApplicationIds] = useState([]);
const [showVerify, setShowVerify] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [errors, setErrors] = useState({});
const [inputValueApplication, setInputValueApplication] = useState(''); // Controlled input value for Application ID
const [isServer, setIsServer] = useState(true);
const [selectedQuota, setSelectedQuota] = useState(0);
const applicationOptions = applicationIds.map(app => ({
value: app.id,
label: app.name
}));
const [templateOptions, setTemplateOptions] = useState([]);
const [generateId, setgenerateId] = useState('');
const handleApplicationChange = (selectedOption) => {
const selectedId = selectedOption.value;
const selectedApp = applicationIds.find(app => app.id === parseInt(selectedId));
setApplicationId(selectedOption ? selectedOption.value : '');
if (selectedApp) {
setSelectedQuota(selectedApp.quota);
}
};
const handleTemplateChange = (selectedOption) => {
setTemplateId(selectedOption ? selectedOption.value : '');
};
const handleInputChangeApplication = (newInputValue) => {
// Limit input to 15 characters for Application ID
if (newInputValue.length <= 15) {
setInputValueApplication(newInputValue);
}
};
useEffect(() => {
const fetchData = () => {
setIsLoading(true);
fetch(`${BASE_URL}/application/list`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
})
.then(response => response.json())
.then(appData => {
if (appData.status_code === 200) {
setApplicationIds(appData.details.data);
return fetch(`${BASE_URL}/template/list?type=1`, {
method: 'GET',
headers: {
'accept': 'application/json',
'x-api-key': API_KEY,
},
});
}
setIsServer(false);
setErrorMessage('Failed to fetch application IDs');
throw new Error('Failed to fetch application IDs');
})
.then(response => response.json())
.then(templateData => {
if (templateData.status_code === 200) {
const templates = templateData.details.data.map(template => ({
value: template.id,
label: template.name
}));
setTemplateOptions(templates);
setIsServer(true);
} else {
setIsServer(false);
setErrorMessage('Failed to fetch templates');
throw new Error('Failed to fetch templates');
}
})
.catch(error => {
console.error('Error:', error);
setIsServer(false);
setErrorMessage(error.message || 'Server connection failed');
})
.finally(() => {
setIsLoading(false);
});
};
fetchData();
}, []);
// Server Down Component
if (!isServer) {
return (
<div style={{ textAlign: 'center', marginTop: '50px' }}>
<img
src={ServerDownAnimation}
alt="Server Down Animation"
style={{ width: '18rem', height: '18rem', marginBottom: '20px' }}
/>
<h2 style={{ color: 'red' }}>Server tidak dapat diakses</h2>
<p>{errorMessage || 'Silakan periksa koneksi internet Anda atau coba lagi nanti.'}</p>
<button
onClick={() => window.location.reload()}
style={{
padding: '10px 20px',
backgroundColor: '#0542cc',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer'
}}>
Coba Lagi
</button>
</div>
);
}
const handleVerify = () => {
setShowPreview(true); // Show the preview when verified
};
const validate = () => {
const newErrors = {};
if (!applicationId) newErrors.applicationId = "Application ID is required.";
if (expiryId <= 0) newErrors.expiryId = "Expiry Time must be greater than 0.";
if (!otpId) newErrors.otpId = "OTP Length is required.";
if (!phoneId) newErrors.phoneId = "Phone Number is required.";
if (!templateId) newErrors.templateId = "Template Name is required.";
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleClick = async () => {
if (validate()) {
setIsLoading(true);
const requestData = {
application_id: parseInt(applicationId),
phone: phoneId,
digits: parseInt(otpId),
interval: parseInt(expiryId),
template_id: parseInt(templateId),
is_test: true,
mode_id: 9
};
console.log('Request Data:', requestData);
// Add timeout to ensure channel stays open
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 30000)
);
Promise.race([
fetch(`${BASE_URL}/wa/otp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify(requestData)
}),
timeoutPromise
])
.then(response => response.json())
.then(data => {
console.log('API Response:', data);
if (data.status_code === 201 && data.details.message === "Successfully") {
setgenerateId(data.details.data.id);
setShowVerify(false);
alert('Authentication request successful!');
}
})
.catch(error => {
console.error('Request failed:', error);
})
.finally(() => {
setIsLoading(false);
});
}
};
return (
<>
<div style={styles.section}>
{/* Inject keyframes for the spinner */}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{isLoading && (
<div style={styles.loadingOverlay}>
<div style={styles.spinner}></div>
</div>
)}
{/* Application ID Selection */}
<div className="form-group row align-items-center">
<div className="col-md-6">
<Select
id="applicationId"
value={applicationOptions.find(option => option.value === applicationId)}
onChange={handleApplicationChange}
options={applicationOptions}
placeholder="Select Application ID"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
inputValue={inputValueApplication}
onInputChange={handleInputChangeApplication} // Limit input length for Application ID
/>
</div>
<div className="col-md-6">
<p className="text-secondary" style={{ fontSize: '16px', fontWeight: '400', margin: '0', marginTop: '8px' }}>
Remaining Quota
</p>
<div style={styles.remainingQuota}>
<span style={styles.quotaText}>{selectedQuota}</span>
<span style={styles.timesText}>(times)</span>
</div>
</div>
</div>
{/* Expiry and OTP */}
<div className="form-group row align-items-center">
{/* Expiry ID/Interval */}
<div className="col-md-6">
<div style={styles.selectWrapper}>
<input
type="number"
id="expiryId"
className="form-control"
value={expiryId === 0 ? '' : expiryId}
onChange={(e) => {
const value = e.target.value;
if (/^[0-9]+$/.test(value)) {
setExpiryId(parseInt(value, 10));
} else if (value === '') {
setExpiryId(0);
}
}}
placeholder="Expiry Time"
/>
{errors.expiryId && <small className="text-danger">{errors.expiryId}</small>}
</div>
</div>
{/* Digits */}
<div className="col-md-6">
<select
id="otpId"
className="form-control"
value={otpId}
onChange={(e) => setOtpId(e.target.value)}
>
<option value="">Select OTP Length</option>
{[1, 2, 3, 4, 5, 6, 7, 8].map(length => (
<option key={length} value={length}>
{length}
</option>
))}
</select>
{errors.otpId && <small className="text-danger">{errors.otpId}</small>}
</div>
</div>
{/* Message and Phone */}
<div className="form-group row align-items-center mt-4">
<div className="col-md-6">
<div style={styles.selectWrapper}>
<Select
id="templateId"
value={templateOptions.find(option => option.value === templateId)}
onChange={handleTemplateChange}
options={templateOptions}
placeholder="Select Template"
isSearchable
menuPortalTarget={document.body}
menuPlacement="auto"
/>
{errors.templateId && <small className="text-danger">{errors.templateId}</small>}
</div>
</div>
<div className="col-md-6">
<div className="input-group">
<span className="input-group-prepend">
<span className="input-group-text">Phone Number</span>
</span>
<input
type="text"
id="phoneId"
className="form-control"
placeholder="Phone Number"
value={phoneId}
onChange={(e) => {
const value = e.target.value;
// Allow only digits and enforce length constraints
if (/^\d*$/.test(value) && value.length <= 14) {
setPhoneId(value);
}
}}
minLength={9} // Enforce minimum length
maxLength={14} // Enforce maximum length
/>
</div>
{errors.phoneId && <small className="text-danger">{errors.phoneId}</small>}
</div>
</div>
{/* Submit Button */}
<div style={styles.submitButton}>
<button onClick={handleClick} className="btn d-flex justify-content-center align-items-center me-2" style={{ backgroundColor: '#0542CC' }}>
<p className="text-white mb-0">Make Authentication</p>
</button>
</div>
</div>
{showVerify && <Verify phoneId={phoneId} onVerify={handleVerify} generateId={generateId} />}
<Preview otpLength={otpId} />
</>
);
}
export default Auth;
const styles = {
section: {
margin: '1rem 0 1rem 0'
},
selectWrapper: {
position: 'relative',
marginTop: '0',
},
select: {
width: '100%',
paddingRight: '30px',
flex: 1,
fontSize: '16px',
border: 'none',
outline: 'none',
},
remainingQuota: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '4px',
},
quotaText: {
fontSize: '40px',
color: '#0542cc',
fontWeight: '600',
},
timesText: {
marginLeft: '8px',
verticalAlign: 'super',
fontSize: '20px',
},
button: {
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0', // Kurangi padding tombol
margin: '0', // Hilangkan margin antar tombol
fontSize: '12px',
},
icon: {
fontSize: '14px',
},
submitButton: {
marginLeft: 'auto',
marginTop: '1.2rem',
textAlign: 'start',
position: 'relative',
zIndex: 1,
},
loadingOverlay: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
spinner: {
border: '4px solid rgba(0, 0, 0, 0.1)',
borderLeftColor: '#0542cc',
borderRadius: '50%',
width: '90px',
height: '90px',
animation: 'spin 1s ease-in-out infinite',
},
preview: {
padding: '20px',
border: '0.1px solid rgba(0, 0, 0, 0.2)',
borderLeft: '4px solid #0542cc',
borderRadius: '10px',
width: '100%',
marginTop: '1rem',
backgroundColor: '#fff',
},
};