fixed known bugs
This commit is contained in:
parent
f967db62a2
commit
1c7f6957ca
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
33
src/utils/pageCache.js
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user