fixed known bugs

This commit is contained in:
MOHAN 2026-03-22 00:32:56 +05:30
parent f967db62a2
commit 1c7f6957ca
15 changed files with 346 additions and 117 deletions

View File

@ -10,7 +10,7 @@ const codes = {
const client = Veriflo.init('your-api-key');
// Send OTP to any WhatsApp number
const { requestId } = await client.sendOTP('+919999999999', {
const { requestId } = await client.sendOTP('+12025550123', {
length: 6, // OTP digit length
expiry: 60, // seconds
});
@ -29,7 +29,7 @@ await client.resendOTP(requestId);`,
client = Veriflo(api_key="your-api-key")
# Send OTP to any WhatsApp number
result = client.send_otp("+919999999999", length=6, expiry=60)
result = client.send_otp("+12025550123", length=6, expiry=60)
# Verify OTP entered by user
is_valid = client.verify_otp(result.request_id, "482916")
@ -44,7 +44,7 @@ client.resend_otp(result.request_id)`,
curl -X POST https://api.veriflo.app/v1/otp/send \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"phone": "+919999999999", "otpLength": 6}'
-d '{"phone": "+12025550123", "otpLength": 6}'
# Response: { "success": true, "requestId": "req_abc123" }
@ -108,3 +108,4 @@ export default function CodeExamples() {
</section>
)
}

View File

@ -29,7 +29,7 @@ const faqs = [
},
{
q: 'What phone number formats are accepted?',
a: 'We accept E.164 format (+91XXXXXXXXXX). The number must have WhatsApp installed. We support all countries where WhatsApp operates.',
a: 'We accept E.164 format (+<country_code><number>). The number must have WhatsApp installed. We support all countries where WhatsApp operates.',
},
{
q: 'Can users request OTP resend?',
@ -112,3 +112,4 @@ export default function FAQ() {
</section>
)
}

View File

@ -7,7 +7,7 @@ import { marketingConfig } from '../config/marketingConfig'
const heroCode = `import Veriflo from 'veriflo';
const client = Veriflo.init('YOUR_API_KEY');
await client.sendOTP('+919999999999');`
await client.sendOTP('+12025550123');`
const OTP_SEQUENCES = ['4821', '738291', '92847165', '5173', '294817', '83726105', '6291', '582047', '17495820']
const HERO_HEADLINES = [
@ -236,3 +236,4 @@ export default function Hero() {
</section>
)
}

View File

@ -355,6 +355,53 @@
background-color: rgba(15, 23, 42, 0.04) !important;
}
[data-theme='light'] .bg-white\/12 {
background-color: rgba(15, 23, 42, 0.07) !important;
}
[data-theme='light'] .hover\:bg-white\/5:hover,
[data-theme='light'] .hover\:bg-white\/8:hover,
[data-theme='light'] .hover\:bg-white\/10:hover,
[data-theme='light'] .hover\:bg-white\/12:hover {
background-color: rgba(15, 23, 42, 0.08) !important;
}
[data-theme='light'] .hover\:text-white:hover,
[data-theme='light'] .focus\:text-white:focus {
color: var(--color-text-primary) !important;
}
[data-theme='light'] .group:hover .group-hover\:text-white {
color: var(--color-text-primary) !important;
}
[data-theme='light'] .group:hover .group-hover\:text-white\/10,
[data-theme='light'] .group:hover .group-hover\:text-white\/5 {
color: rgba(15, 23, 42, 0.16) !important;
}
[data-theme='light'] .glass-panel {
background: rgba(255,255,255,0.7);
border-color: rgba(15,23,42,0.12);
}
[data-theme='light'] .glass-panel:hover {
background: rgba(255,255,255,0.88);
border-color: rgba(15,23,42,0.18);
}
[data-theme='light'] .solid-panel {
background: #f8fbff;
border-color: rgba(15,23,42,0.1);
box-shadow: 0 14px 40px rgba(15,23,42,0.12);
}
[data-theme='light'] .btn-icon:hover {
color: var(--color-text-primary);
background: rgba(15,23,42,0.08);
border-color: rgba(15,23,42,0.18);
}
/* Animations */
@keyframes float {
0%, 100% { transform: translateY(0px); }

View File

@ -3,6 +3,7 @@ import { Plus, Copy, Check, Trash2, Loader2, AlertCircle, Download, KeyRound, Ci
import api from '../services/api'
import { useNavigate } from 'react-router-dom'
import { useToast } from '../components/ToastProvider'
import { readPageCache, writePageCache } from '../utils/pageCache'
function formatDateTime(value) {
if (!value) return 'Never'
@ -62,7 +63,7 @@ const HELP_TOPICS = [
content: `curl -X POST http://localhost:3000/v1/otp/send \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{"phone":"+919999999999"}'`,
-d '{"phone":"+12025550123"}'`,
},
{
id: 'best-practices',
@ -111,7 +112,7 @@ const HELP_TOPICS = [
code: `const Veriflo = require('veriflo');
const client = Veriflo.init('YOUR_API_KEY');
const result = await client.sendOTP('+919999999999', { length: 6 });
const result = await client.sendOTP('+12025550123', { length: 6 });
console.log(result.request_id);`,
},
{
@ -119,7 +120,7 @@ console.log(result.request_id);`,
code: `from veriflo import Veriflo
client = Veriflo(api_key="YOUR_API_KEY")
result = client.send_otp("+919999999999", length=6)
result = client.send_otp("+12025550123", length=6)
print(result.request_id)`,
},
{
@ -135,7 +136,7 @@ Content-Type: application/json`,
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({ phone: '+919999999999' })
body: JSON.stringify({ phone: '+12025550123' })
});`,
},
],
@ -143,10 +144,11 @@ Content-Type: application/json`,
]
export default function APIKeys() {
const cached = readPageCache('api_keys_page', 90 * 1000)
const navigate = useNavigate()
const toast = useToast()
const [keys, setKeys] = useState([])
const [loading, setLoading] = useState(true)
const [keys, setKeys] = useState(cached?.keys || [])
const [loading, setLoading] = useState(!cached)
const [generating, setGenerating] = useState(false)
const [deletingId, setDeletingId] = useState(null)
const [copied, setCopied] = useState('')
@ -154,9 +156,9 @@ export default function APIKeys() {
const [error, setError] = useState('')
const [keyName, setKeyName] = useState('')
const [keyMode, setKeyMode] = useState('sandbox')
const [keyLimitPerMode, setKeyLimitPerMode] = useState(2)
const [keyLimitTotal, setKeyLimitTotal] = useState(4)
const [trialStatus, setTrialStatus] = useState('not_applied')
const [keyLimitPerMode, setKeyLimitPerMode] = useState(cached?.keyLimitPerMode || 2)
const [keyLimitTotal, setKeyLimitTotal] = useState(cached?.keyLimitTotal || 4)
const [trialStatus, setTrialStatus] = useState(cached?.trialStatus || 'not_applied')
const [activeHelpId, setActiveHelpId] = useState('')
const [showCreateModal, setShowCreateModal] = useState(false)
@ -171,25 +173,36 @@ export default function APIKeys() {
const liveKeys = keys.filter((key) => key.mode === 'live')
useEffect(() => {
fetchKeys()
fetchKeys({ silent: Boolean(cached) })
}, [])
const fetchKeys = async () => {
setLoading(true)
const fetchKeys = async ({ silent = false } = {}) => {
if (!silent) setLoading(true)
try {
const profileRes = await api.get('/api/user/profile')
setTrialStatus(profileRes.data.plan?.trial_status || 'not_applied')
const nextTrialStatus = profileRes.data.plan?.trial_status || 'not_applied'
setTrialStatus(nextTrialStatus)
const res = await api.get('/api/user/api-keys')
setKeys(res.data.keys || [])
setKeyLimitPerMode(res.data.limit_per_mode || 2)
setKeyLimitTotal(res.data.limit_total || 4)
const nextKeys = res.data.keys || []
const nextLimitPerMode = res.data.limit_per_mode || 2
const nextLimitTotal = res.data.limit_total || 4
setKeys(nextKeys)
setKeyLimitPerMode(nextLimitPerMode)
setKeyLimitTotal(nextLimitTotal)
writePageCache('api_keys_page', {
keys: nextKeys,
keyLimitPerMode: nextLimitPerMode,
keyLimitTotal: nextLimitTotal,
trialStatus: nextTrialStatus,
})
} catch (err) {
const message = err.response?.data?.error || 'Failed to load keys'
setError(message)
toast.error(message)
} finally {
setLoading(false)
if (!silent) setLoading(false)
}
}
@ -567,3 +580,4 @@ export default function APIKeys() {
</div>
)
}

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
import { Download, RefreshCw, AlertCircle, Loader2 } from 'lucide-react'
import api from '../services/api'
import { readPageCache, writePageCache } from '../utils/pageCache'
export default function Analytics() {
const [data, setData] = useState(null)
@ -16,15 +17,34 @@ export default function Analytics() {
}, [page, pageSize])
const fetchData = async () => {
setLoading(true)
const cacheKey = `analytics_page_${page}_${pageSize}`
const cached = readPageCache(cacheKey, 75 * 1000)
if (cached) {
setData(cached.data || null)
setLogs(cached.logs || [])
setPagination(cached.pagination || { total: 0, page: 1, page_size: pageSize, total_pages: 1, has_prev: false, has_next: false })
setLoading(false)
} else {
setLoading(true)
}
try {
const [summaryRes, logsRes] = await Promise.all([
api.get('/api/user/analytics/summary'),
api.get(`/api/user/analytics/logs?page=${page}&page_size=${pageSize}`)
])
setData(summaryRes.data)
setLogs(logsRes.data.logs)
setPagination(logsRes.data.pagination || { total: 0, page: 1, page_size: pageSize, total_pages: 1, has_prev: false, has_next: false })
const nextData = summaryRes.data
const nextLogs = logsRes.data.logs
const nextPagination = logsRes.data.pagination || { total: 0, page: 1, page_size: pageSize, total_pages: 1, has_prev: false, has_next: false }
setData(nextData)
setLogs(nextLogs)
setPagination(nextPagination)
writePageCache(cacheKey, {
data: nextData,
logs: nextLogs,
pagination: nextPagination,
})
} catch (err) {
console.error("Failed to fetch analytics", err)
} finally {

View File

@ -4,19 +4,22 @@ import api from '../services/api'
import { useToast } from '../components/ToastProvider'
import { getAuthUser } from '../utils/authSession'
import { useNavigate } from 'react-router-dom'
import { readPageCache, writePageCache } from '../utils/pageCache'
export default function Billing() {
const cached = readPageCache('billing_page', 90 * 1000)
const navigate = useNavigate()
const toast = useToast()
const [stats, setStats] = useState(null)
const [plan, setPlan] = useState(null)
const [limits, setLimits] = useState(null)
const [billingConfig, setBillingConfig] = useState(null)
const [payments, setPayments] = useState([])
const [loading, setLoading] = useState(true)
const [stats, setStats] = useState(cached?.stats || null)
const [plan, setPlan] = useState(cached?.plan || null)
const [limits, setLimits] = useState(cached?.limits || null)
const [billingConfig, setBillingConfig] = useState(cached?.billingConfig || null)
const [payments, setPayments] = useState(cached?.payments || [])
const [loading, setLoading] = useState(!cached)
const [actionLoading, setActionLoading] = useState(false)
const [payingCode, setPayingCode] = useState('')
const [selectedPackageCode, setSelectedPackageCode] = useState('growth')
const [showTrialApplyModal, setShowTrialApplyModal] = useState(false)
const [showQuotaHelp, setShowQuotaHelp] = useState(false)
const [showPurchaseHelp, setShowPurchaseHelp] = useState(false)
const [showHistoryHelp, setShowHistoryHelp] = useState(false)
@ -26,48 +29,46 @@ export default function Billing() {
}, [])
const fetchBillingInfo = async () => {
const silent = Boolean(stats && plan && limits)
if (!silent) setLoading(true)
try {
const [profileRes, paymentsRes, billingConfigRes] = await Promise.all([
api.get('/api/user/profile'),
api.get('/api/user/billing/payments'),
api.get('/api/user/billing/config'),
])
setStats(profileRes.data.stats)
setPlan(profileRes.data.plan)
setLimits(profileRes.data.limits)
setBillingConfig(billingConfigRes.data.config || null)
setPayments(paymentsRes.data.payments || [])
const nextStats = profileRes.data.stats
const nextPlan = profileRes.data.plan
const nextLimits = profileRes.data.limits
const nextBillingConfig = billingConfigRes.data.config || null
const nextPayments = paymentsRes.data.payments || []
setStats(nextStats)
setPlan(nextPlan)
setLimits(nextLimits)
setBillingConfig(nextBillingConfig)
setPayments(nextPayments)
writePageCache('billing_page', {
stats: nextStats,
plan: nextPlan,
limits: nextLimits,
billingConfig: nextBillingConfig,
payments: nextPayments,
})
} catch (err) {
console.error("Failed to fetch billing info", err)
} finally {
setLoading(false)
if (!silent) setLoading(false)
}
}
const handleApply = async () => {
const confirmed = window.confirm(
[
'Activate Free Trial?',
'',
'By continuing, you agree to the free trial rules:',
`1. Sandbox trial quota: ${stats?.trial_total_credits || 500} OTPs/month (self WhatsApp number only).`,
`2. Live starter quota: ${stats?.live_total_credits || 100} OTPs/month (can send to other valid numbers).`,
'3. Sandbox and Live use separate API key sets and separate usage counters.',
'4. Misuse, spam, or policy violations may suspend trial access.',
'',
'Click OK to agree and activate now.'
].join('\n')
)
if (!confirmed) {
return
}
setActionLoading(true)
try {
await api.post('/api/user/trial/apply')
const res = await api.post('/api/user/trial/activate')
toast.success(res.data.message || 'Free trial activated successfully')
setShowTrialApplyModal(false)
await fetchBillingInfo()
} catch (err) {
toast.error(err.response?.data?.error || 'Failed to activate free trial')
@ -209,8 +210,8 @@ export default function Billing() {
<div className="mt-5">
{plan?.trial_status === 'not_applied' && (
<button onClick={handleApply} disabled={actionLoading} className="neu-btn-accent px-6 py-3 w-full md:w-auto">
{actionLoading ? <Loader2 size={16} className="animate-spin" /> : 'Apply Free Trial'}
<button onClick={() => setShowTrialApplyModal(true)} disabled={actionLoading} className="neu-btn-accent px-6 py-3 w-full md:w-auto">
Apply Free Trial
</button>
)}
{plan?.trial_status === 'applied' && (
@ -358,6 +359,61 @@ export default function Billing() {
</div>
) : null}
{showTrialApplyModal ? (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<button
type="button"
onClick={() => !actionLoading && setShowTrialApplyModal(false)}
className="absolute inset-0 bg-black/70 border-none cursor-pointer"
/>
<div className="relative w-full max-w-[760px] rounded-2xl p-6 neu-raised" style={{ background: 'var(--color-base)' }}>
<div className="flex items-center justify-between gap-3 mb-4">
<h2 className="text-lg font-black text-text-primary m-0">Activate Free Trial</h2>
<button
type="button"
onClick={() => setShowTrialApplyModal(false)}
disabled={actionLoading}
className="neu-btn p-2"
>
<X size={14} />
</button>
</div>
<div className="grid gap-3 text-sm font-semibold text-text-secondary">
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
1. Sandbox trial quota: <b>{stats?.trial_total_credits || 500}</b> OTPs/month (self WhatsApp number only).
</div>
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
2. Live starter quota: <b>{stats?.live_total_credits || 100}</b> OTPs/month (can send to valid external numbers).
</div>
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
3. Sandbox and Live use separate API key sets and separate usage counters.
</div>
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>
4. Misuse, spam, or policy violations may suspend trial access.
</div>
</div>
<div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end">
<button
type="button"
onClick={() => setShowTrialApplyModal(false)}
disabled={actionLoading}
className="neu-btn px-5 py-2.5"
>
Cancel
</button>
<button
type="button"
onClick={handleApply}
disabled={actionLoading}
className="neu-btn-accent px-5 py-2.5"
>
{actionLoading ? <span className="inline-flex items-center gap-2"><Loader2 size={14} className="animate-spin" /> Activating...</span> : 'Agree & Activate Trial'}
</button>
</div>
</div>
</div>
) : null}
{showPurchaseHelp ? (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<button type="button" onClick={() => setShowPurchaseHelp(false)} className="absolute inset-0 bg-black/70 border-none cursor-pointer" />

View File

@ -15,7 +15,7 @@ const sdkCatalog = [
type: 'Official SDK',
install: 'npm install veriflo',
setupLanguage: 'javascript',
setup: `import Veriflo from 'veriflo'\n\nconst client = Veriflo.init(process.env.VERIFLO_API_KEY)\n\nconst otpRequest = await client.sendOTP('+919999999999', {\n length: 6,\n expiry: 60,\n})`,
setup: `import Veriflo from 'veriflo'\n\nconst client = Veriflo.init(process.env.VERIFLO_API_KEY)\n\nconst otpRequest = await client.sendOTP('+12025550123', {\n length: 6,\n expiry: 60,\n})`,
packageUrl: 'https://www.npmjs.com/package/veriflo',
packageLabel: 'npm package',
downloadUrl: '/downloads/veriflo-node-starter.js',
@ -32,7 +32,7 @@ const sdkCatalog = [
type: 'Official SDK',
install: 'pip install veriflo',
setupLanguage: 'python',
setup: `from veriflo import Veriflo\nimport os\n\nclient = Veriflo(api_key=os.environ['VERIFLO_API_KEY'])\n\nresult = client.send_otp('+919999999999', length=6, expiry=60)`,
setup: `from veriflo import Veriflo\nimport os\n\nclient = Veriflo(api_key=os.environ['VERIFLO_API_KEY'])\n\nresult = client.send_otp('+12025550123', length=6, expiry=60)`,
packageUrl: 'https://pypi.org/project/veriflo',
packageLabel: 'PyPI package',
downloadUrl: '/downloads/veriflo-python-starter.py',
@ -49,7 +49,7 @@ const sdkCatalog = [
type: 'REST Starter',
install: 'No SDK required. Use built-in cURL support or your preferred HTTP client.',
setupLanguage: 'php',
setup: `<?php\n\n$apiKey = getenv('VERIFLO_API_KEY');\n$payload = json_encode([\n 'phone' => '+919999999999',\n 'otpLength' => 6,\n]);`,
setup: `<?php\n\n$apiKey = getenv('VERIFLO_API_KEY');\n$payload = json_encode([\n 'phone' => '+12025550123',\n 'otpLength' => 6,\n]);`,
packageUrl: 'https://api.veriflo.app/v1/otp/send',
packageLabel: 'REST endpoint',
downloadUrl: '/downloads/veriflo-php-starter.php',
@ -83,7 +83,7 @@ const sdkCatalog = [
type: 'REST Starter',
install: 'No SDK required. Uses Ruby standard libraries plus JSON.',
setupLanguage: 'ruby',
setup: `curl -X POST https://api.veriflo.app/v1/otp/send \\\n+ -H \"Authorization: Bearer \${VERIFLO_API_KEY}\" \\\n+ -H \"Content-Type: application/json\" \\\n+ -d '{\"phone\":\"+919999999999\",\"otpLength\":6}'`,
setup: `curl -X POST https://api.veriflo.app/v1/otp/send \\\n+ -H \"Authorization: Bearer \${VERIFLO_API_KEY}\" \\\n+ -H \"Content-Type: application/json\" \\\n+ -d '{\"phone\":\"+12025550123\",\"otpLength\":6}'`,
packageUrl: 'https://api.veriflo.app/v1/otp/send',
packageLabel: 'REST endpoint',
downloadUrl: '/downloads/veriflo-ruby-starter.rb',
@ -117,7 +117,7 @@ const sdkCatalog = [
type: 'HTTP Starter',
install: 'No SDK required. Works anywhere you can issue HTTPS requests.',
setupLanguage: 'bash',
setup: `curl -X POST https://api.veriflo.app/v1/otp/send \\\n+ -H \"Authorization: Bearer \${VERIFLO_API_KEY}\" \\\n+ -H \"Content-Type: application/json\" \\\n+ -d '{\"phone\":\"+919999999999\",\"otpLength\":6}'`,
setup: `curl -X POST https://api.veriflo.app/v1/otp/send \\\n+ -H \"Authorization: Bearer \${VERIFLO_API_KEY}\" \\\n+ -H \"Content-Type: application/json\" \\\n+ -d '{\"phone\":\"+12025550123\",\"otpLength\":6}'`,
packageUrl: 'https://api.veriflo.app/v1/otp/send',
packageLabel: 'REST endpoint',
downloadUrl: '/downloads/veriflo-curl-starter.sh',
@ -277,7 +277,7 @@ function GettingStarted() {
Send your first OTP
</h3>
<div className="ml-11">
<CodeBlock code={`import Veriflo from 'veriflo';\nconst client = Veriflo.init('YOUR_API_KEY');\nawait client.sendOTP('+919999999999');`} language="javascript" />
<CodeBlock code={`import Veriflo from 'veriflo';\nconst client = Veriflo.init('YOUR_API_KEY');\nawait client.sendOTP('+12025550123');`} language="javascript" />
</div>
</div>
</DocSection>
@ -317,7 +317,7 @@ function SendOTP() {
<CodeBlock code={`curl -X POST https://api.veriflo.app/v1/otp/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone": "+919999999999", "otpLength": 6}'`} language="bash" />
-d '{"phone": "+12025550123", "otpLength": 6}'`} language="bash" />
<h3 className="font-bold text-white text-xl mt-10 mb-4 border-b border-white/10 pb-2">Response</h3>
<CodeBlock code={`{\n "success": true,\n "requestId": "req_abc123",\n "expiresAt": 1700000060\n}`} language="json" />
</DocSection>
@ -581,3 +581,4 @@ function RateLimits() {
)
}

View File

@ -34,14 +34,29 @@ export default function Integrate() {
return () => { mounted = false }
}, [])
const normalizeE164 = (value) => {
const trimmed = String(value || '').trim()
if (!trimmed) return ''
const digits = trimmed.replace(/\D/g, '').slice(0, 15)
if (!digits) return ''
return `+${digits}`
}
const phoneForApi = normalizeE164(testPhone).replace(/\D/g, '')
const displayPhone = normalizeE164(testPhone) || '+12025550123'
const handleTestSend = async (e) => {
e.preventDefault()
if (phoneForApi.length < 8) {
setError('Enter a valid phone number with country code.')
return
}
setSending(true)
setError('')
setSent(false)
try {
await api.post('/api/user/test-otp', { phone: '91' + testPhone })
await api.post('/api/user/test-otp', { phone: phoneForApi })
setSent(true)
window.dispatchEvent(new CustomEvent('veriflo-usage-updated'))
toast.success('Test OTP sent successfully')
@ -63,7 +78,7 @@ const Veriflo = require('veriflo');
const client = Veriflo.init('${key}');
// Send OTP
const { requestId } = await client.sendOTP('+91${testPhone || '9999999999'}', { length: 6 });
const { requestId } = await client.sendOTP('${displayPhone}', { length: 6 });
// Verify OTP later
const verified = await client.verifyOTP(requestId, '123456');
@ -78,7 +93,7 @@ from veriflo import Veriflo
client = Veriflo(api_key="${key}")
# Send OTP
result = client.send_otp("+91${testPhone || '9999999999'}", length=6)
result = client.send_otp("${displayPhone}", length=6)
is_valid = client.verify_otp(result.request_id, "123456")
@ -88,7 +103,7 @@ print('Verified?', is_valid)`
if(tab === 'HTTP') return `curl -X POST http://localhost:3000/v1/otp/send \\
-H "Authorization: Bearer ${key}" \\
-H "Content-Type: application/json" \\
-d '{"phone": "+91${testPhone || '9999999999'}"}'
-d '{"phone": "${displayPhone}"}'
curl -X POST http://localhost:3000/v1/otp/verify \\
-H "Authorization: Bearer ${key}" \\
@ -189,30 +204,23 @@ curl -X POST http://localhost:3000/v1/otp/verify \\
<form onSubmit={handleTestSend} className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<label className="text-xs font-black uppercase tracking-wider text-text-secondary px-1">Recipient Phone Number</label>
<div className="grid grid-cols-[88px_minmax(0,1fr)] gap-3">
<input
type="text"
value="+91"
readOnly
className="neu-input text-center text-text-muted font-black tracking-wide"
/>
<input
type="text"
required
placeholder="98765 43210"
value={testPhone}
onChange={e => setTestPhone(e.target.value)}
className="neu-input min-w-0 tracking-[0.12em] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/55"
aria-label="Recipient phone number"
inputMode="numeric"
autoComplete="tel-national"
/>
</div>
<input
type="tel"
required
placeholder="+12025550123"
value={testPhone}
onChange={(e) => setTestPhone(e.target.value)}
className="neu-input min-w-0 tracking-[0.08em] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/55"
aria-label="Recipient phone number"
inputMode="tel"
autoComplete="tel"
/>
<div className="text-[11px] font-semibold text-text-muted px-1">Use international format with country code, like +1, +44, +81.</div>
</div>
<button
type="submit"
disabled={sending || sent || !testPhone}
disabled={sending || sent || phoneForApi.length < 8}
className={`py-3.5 gap-2 font-bold shadow-lg transition-all flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/55 focus-visible:ring-offset-1 focus-visible:ring-offset-base ${
sent ? 'neu-raised text-accent pointer-events-none border-none' : 'neu-btn-accent'
}`}
@ -285,7 +293,7 @@ curl -X POST http://localhost:3000/v1/otp/verify \\
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Sandbox mode: only your registered WhatsApp number is allowed.</div>
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Live mode: can send to any valid number under your live quota.</div>
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Rate limit: max {testOtpUserLimit} test requests per minute per account.</div>
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Country code is fixed as +91 in this quick tester.</div>
<div className="rounded-xl p-3" style={{ background: 'rgba(255,255,255,0.03)' }}>Use E.164 format (for example +12025550123) when testing numbers.</div>
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { Save, Eye, Smartphone, Loader2, CheckCircle, ChevronDown, CircleHelp, X } from 'lucide-react'
import api from '../services/api'
import { useToast } from '../components/ToastProvider'
import { readPageCache, writePageCache } from '../utils/pageCache'
const DEFAULT_TEMPLATE = `{greeting} Your {sender_name} verification code is:
@ -29,19 +30,20 @@ function formatDisplayPhone(phone) {
}
export default function MessageConfig() {
const cached = readPageCache('message_config_page', 90 * 1000)
const toast = useToast()
const [loading, setLoading] = useState(true)
const [loading, setLoading] = useState(!cached)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [error, setError] = useState('')
const [registeredPhone, setRegisteredPhone] = useState('')
const [registeredPhone, setRegisteredPhone] = useState(cached?.registeredPhone || '')
const [testingWebhook, setTestingWebhook] = useState(false)
const [webhookNotice, setWebhookNotice] = useState('')
const [messageTemplateLimit, setMessageTemplateLimit] = useState(320)
const [sandboxMonthlyLimit, setSandboxMonthlyLimit] = useState(0)
const [sandboxUsedThisMonth, setSandboxUsedThisMonth] = useState(0)
const [liveMonthlyLimit, setLiveMonthlyLimit] = useState(0)
const [liveUsedThisMonth, setLiveUsedThisMonth] = useState(0)
const [messageTemplateLimit, setMessageTemplateLimit] = useState(cached?.messageTemplateLimit || 320)
const [sandboxMonthlyLimit, setSandboxMonthlyLimit] = useState(cached?.sandboxMonthlyLimit || 0)
const [sandboxUsedThisMonth, setSandboxUsedThisMonth] = useState(cached?.sandboxUsedThisMonth || 0)
const [liveMonthlyLimit, setLiveMonthlyLimit] = useState(cached?.liveMonthlyLimit || 0)
const [liveUsedThisMonth, setLiveUsedThisMonth] = useState(cached?.liveUsedThisMonth || 0)
const [openSections, setOpenSections] = useState({
environment: true,
sender: true,
@ -49,7 +51,7 @@ export default function MessageConfig() {
})
const [showSandboxHelp, setShowSandboxHelp] = useState(false)
const [settings, setSettings] = useState({
const [settings, setSettings] = useState(cached?.settings || {
sender_name: 'Acme Corp',
greeting: 'Hello!',
message_template: DEFAULT_TEMPLATE,
@ -65,28 +67,47 @@ export default function MessageConfig() {
}, [])
const fetchSettings = async () => {
const silent = Boolean(cached)
if (!silent) setLoading(true)
try {
const res = await api.get('/api/user/profile')
setRegisteredPhone(res.data.user?.phone || '')
setMessageTemplateLimit(res.data.limits?.message_template_max_chars || 320)
setSandboxMonthlyLimit(res.data.limits?.sandbox_monthly_message_limit || 0)
setSandboxUsedThisMonth(res.data.stats?.sandbox_used_this_month || 0)
setLiveMonthlyLimit(res.data.limits?.live_monthly_message_limit || 0)
setLiveUsedThisMonth(res.data.stats?.live_used_this_month || 0)
const nextRegisteredPhone = res.data.user?.phone || ''
const nextMessageTemplateLimit = res.data.limits?.message_template_max_chars || 320
const nextSandboxMonthlyLimit = res.data.limits?.sandbox_monthly_message_limit || 0
const nextSandboxUsed = res.data.stats?.sandbox_used_this_month || 0
const nextLiveMonthlyLimit = res.data.limits?.live_monthly_message_limit || 0
const nextLiveUsed = res.data.stats?.live_used_this_month || 0
setRegisteredPhone(nextRegisteredPhone)
setMessageTemplateLimit(nextMessageTemplateLimit)
setSandboxMonthlyLimit(nextSandboxMonthlyLimit)
setSandboxUsedThisMonth(nextSandboxUsed)
setLiveMonthlyLimit(nextLiveMonthlyLimit)
setLiveUsedThisMonth(nextLiveUsed)
if (res.data.settings) {
setSettings({
const nextSettings = {
...res.data.settings,
message_template: res.data.settings.message_template || DEFAULT_TEMPLATE,
otp_length: String(res.data.settings.otp_length),
expiry_seconds: String(res.data.settings.expiry_seconds),
environment_mode: res.data.settings.environment_mode || 'sandbox'
}
setSettings(nextSettings)
writePageCache('message_config_page', {
registeredPhone: nextRegisteredPhone,
messageTemplateLimit: nextMessageTemplateLimit,
sandboxMonthlyLimit: nextSandboxMonthlyLimit,
sandboxUsedThisMonth: nextSandboxUsed,
liveMonthlyLimit: nextLiveMonthlyLimit,
liveUsedThisMonth: nextLiveUsed,
settings: nextSettings,
})
}
} catch (err) {
console.error("Failed to fetch settings", err)
setError('Failed to load settings')
} finally {
setLoading(false)
if (!silent) setLoading(false)
}
}

View File

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { Send, CheckCircle, Clock, Key, CreditCard, Loader2, AlertTriangle, Activity, ShieldCheck, Users, TrendingUp, CircleHelp, X } from 'lucide-react'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts'
import api from '../services/api'
import { readPageCache, writePageCache } from '../utils/pageCache'
function getRelativeTime(value) {
const timestamp = new Date(value).getTime()
@ -40,8 +41,9 @@ function percentage(numerator, denominator) {
}
export default function Overview() {
const [stats, setStats] = useState(null)
const [loading, setLoading] = useState(true)
const cachedSummary = readPageCache('overview_summary', 75 * 1000)
const [stats, setStats] = useState(cachedSummary || null)
const [loading, setLoading] = useState(!cachedSummary)
const [period, setPeriod] = useState('7D')
const [showPerformanceHelp, setShowPerformanceHelp] = useState(false)
const [showAllActivityModal, setShowAllActivityModal] = useState(false)
@ -55,7 +57,10 @@ export default function Overview() {
const fetchData = async () => {
try {
const res = await api.get('/api/user/analytics/summary')
if (isMounted) setStats(res.data)
if (isMounted) {
setStats(res.data)
writePageCache('overview_summary', res.data)
}
} catch (err) {
console.error('Failed to fetch analytics summary', err)
} finally {

View File

@ -4,20 +4,22 @@ import { useNavigate } from 'react-router-dom'
import api from '../services/api'
import { clearAuthSession, getAuthUser, updateStoredUser } from '../utils/authSession'
import { useToast } from '../components/ToastProvider'
import { readPageCache, writePageCache } from '../utils/pageCache'
export default function Settings() {
const cached = readPageCache('settings_page', 90 * 1000)
const toast = useToast()
const navigate = useNavigate()
const [loading, setLoading] = useState(true)
const [loading, setLoading] = useState(!cached)
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [error, setError] = useState('')
const [loggingOut, setLoggingOut] = useState(false)
const [showProfileHelp, setShowProfileHelp] = useState(false)
const [showSessionHelp, setShowSessionHelp] = useState(false)
const [billingConfig, setBillingConfig] = useState(null)
const [billingConfig, setBillingConfig] = useState(cached?.billingConfig || null)
const [formData, setFormData] = useState({
const [formData, setFormData] = useState(cached?.formData || {
name: '',
email: '',
company: '',
@ -29,24 +31,32 @@ export default function Settings() {
}, [])
const fetchProfile = async () => {
const silent = Boolean(cached)
if (!silent) setLoading(true)
try {
const [profileRes, billingRes] = await Promise.all([
api.get('/api/user/profile'),
api.get('/api/user/billing/config')
])
const { name, email, company } = profileRes.data.user
setFormData({
const nextFormData = {
name: name || '',
email: email || '',
company: company || '',
new_password: ''
}
const nextBillingConfig = billingRes.data?.config || null
setFormData(nextFormData)
setBillingConfig(nextBillingConfig)
writePageCache('settings_page', {
formData: nextFormData,
billingConfig: nextBillingConfig,
})
setBillingConfig(billingRes.data?.config || null)
} catch (err) {
console.error("Failed to fetch profile", err)
setError('Failed to load profile')
} finally {
setLoading(false)
if (!silent) setLoading(false)
}
}
@ -65,6 +75,15 @@ export default function Settings() {
company: updatedUser.company || '',
new_password: ''
})
writePageCache('settings_page', {
formData: {
name: updatedUser.name || '',
email: updatedUser.email || '',
company: updatedUser.company || '',
new_password: '',
},
billingConfig,
})
setSaved(true)
toast.success('Profile updated successfully')
setTimeout(() => setSaved(false), 3000)

View File

@ -198,7 +198,7 @@ export default function ForgotPassword() {
value={formData.phone}
onChange={handleChange}
required
placeholder="+919999999999"
placeholder="+12025550123"
className="flex-1 bg-transparent border-none outline-none py-3 text-sm font-semibold text-text-primary placeholder:text-text-muted"
/>
</div>
@ -300,3 +300,4 @@ export default function ForgotPassword() {
</div>
)
}

View File

@ -254,7 +254,7 @@ export default function Signup() {
value={formData.phone}
onChange={handleChange}
required
placeholder="+919999999999"
placeholder="+12025550123"
className="flex-1 bg-transparent border-none outline-none py-3 text-sm font-semibold text-text-primary placeholder:text-text-muted"
/>
</div>
@ -387,3 +387,4 @@ export default function Signup() {
</div>
)
}

33
src/utils/pageCache.js Normal file
View File

@ -0,0 +1,33 @@
const CACHE_PREFIX = 'veriflo_page_cache_v1:'
export function readPageCache(key, maxAgeMs = 60 * 1000) {
try {
const raw = sessionStorage.getItem(`${CACHE_PREFIX}${key}`)
if (!raw) return null
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object') return null
const ageMs = Date.now() - Number(parsed.savedAt || 0)
if (!Number.isFinite(ageMs) || ageMs > maxAgeMs) return null
return parsed.data ?? null
} catch {
return null
}
}
export function writePageCache(key, data) {
try {
sessionStorage.setItem(
`${CACHE_PREFIX}${key}`,
JSON.stringify({
savedAt: Date.now(),
data,
})
)
} catch {
// Ignore cache write failures (private mode/storage full)
}
}