585 lines
21 KiB
JavaScript
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',
|
|
},
|
|
};
|