BigCommerce
Email checker for BigCommerce. Verify customer emails at checkout and registration.
EmailVerify를 BigCommerce와 통합하여 결제 시 고객 이메일을 검증하고, 사기를 줄이고, 주문 확인 및 마케팅 이메일의 전달률을 개선하세요.
통합 방법
| 방법 | 적합 대상 | 복잡도 |
|---|---|---|
| Script Manager | 빠른 설정 | 낮음 |
| Stencil 테마 | 커스텀 테마 | 중간 |
| Storefront API | 헤드리스 스토어 | 높음 |
방법 1: Script Manager 통합
BigCommerce Script Manager를 사용하여 실시간 이메일 검증을 추가합니다.
검증 스크립트 생성
- Storefront → Script Manager로 이동
- Create a Script 클릭
- 구성:
- Name: EmailVerify Email Verification
- Location: Footer
- Pages: Checkout
스크립트 코드
<script>
(function() {
const EMAILVERIFY_API_KEY = 'YOUR_API_KEY';
// Wait for checkout to load
function waitForCheckout() {
const emailInput = document.querySelector('[data-test="customer-email"] input, #email');
if (emailInput) {
initVerification(emailInput);
} else {
setTimeout(waitForCheckout, 500);
}
}
function initVerification(emailInput) {
const statusEl = document.createElement('span');
statusEl.className = 'bv-status';
emailInput.parentNode.appendChild(statusEl);
let verifyTimeout;
let lastEmail = '';
emailInput.addEventListener('blur', function() {
const email = this.value;
if (!email || email === lastEmail || !isValidFormat(email)) {
return;
}
clearTimeout(verifyTimeout);
verifyTimeout = setTimeout(function() {
verifyEmail(email, statusEl);
lastEmail = email;
}, 500);
});
}
async function verifyEmail(email, statusEl) {
statusEl.textContent = 'Verifying...';
statusEl.className = 'bv-status verifying';
try {
const response = await fetch('https://api.emailverify.ai/v1/verify', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + EMAILVERIFY_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: email })
});
const data = await response.json();
if (data.status === 'valid') {
statusEl.textContent = '✓ Email verified';
statusEl.className = 'bv-status valid';
} else if (data.status === 'invalid') {
statusEl.textContent = '✗ Please enter a valid email';
statusEl.className = 'bv-status invalid';
} else if (data.result && data.result.disposable) {
statusEl.textContent = '✗ Please use a permanent email';
statusEl.className = 'bv-status invalid';
} else {
statusEl.textContent = '';
statusEl.className = 'bv-status';
}
} catch (error) {
console.error('Verification error:', error);
statusEl.textContent = '';
statusEl.className = 'bv-status';
}
}
function isValidFormat(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Add styles
const style = document.createElement('style');
style.textContent = `
.bv-status {
display: block;
margin-top: 4px;
font-size: 12px;
}
.bv-status.verifying { color: #666; }
.bv-status.valid { color: #28a745; }
.bv-status.invalid { color: #dc3545; }
`;
document.head.appendChild(style);
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitForCheckout);
} else {
waitForCheckout();
}
})();
</script>방법 2: Stencil 테마 통합
커스텀 Stencil 테마의 경우 테마 파일에 직접 검증을 추가합니다.
테마 컴포넌트 생성
// assets/js/theme/global/email-verification.js
import utils from '@bigcommerce/stencil-utils';
export default class EmailVerification {
constructor() {
this.apiKey = window.emailverifyConfig?.apiKey;
this.emailInputs = document.querySelectorAll('input[type="email"]');
this.cache = new Map();
if (this.apiKey && this.emailInputs.length) {
this.init();
}
}
init() {
this.emailInputs.forEach(input => {
this.attachVerification(input);
});
}
attachVerification(input) {
const statusEl = document.createElement('span');
statusEl.className = 'form-field-verification';
input.parentNode.appendChild(statusEl);
let timeout;
input.addEventListener('blur', () => {
const email = input.value.trim();
if (!email || !this.isValidFormat(email)) {
this.clearStatus(statusEl);
return;
}
clearTimeout(timeout);
timeout = setTimeout(() => this.verify(email, statusEl, input), 500);
});
}
async verify(email, statusEl, input) {
// Check cache
if (this.cache.has(email)) {
this.displayResult(this.cache.get(email), statusEl, input);
return;
}
this.showLoading(statusEl);
try {
const response = await fetch('https://api.emailverify.ai/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email })
});
const data = await response.json();
this.cache.set(email, data);
this.displayResult(data, statusEl, input);
} catch (error) {
console.error('Email verification error:', error);
this.clearStatus(statusEl);
}
}
displayResult(data, statusEl, input) {
input.classList.remove('is-valid', 'is-invalid');
if (data.status === 'valid' && !data.result?.disposable) {
statusEl.innerHTML = '<span class="icon icon--success"></span> Verified';
statusEl.className = 'form-field-verification is-valid';
input.classList.add('is-valid');
} else if (data.status === 'invalid') {
statusEl.innerHTML = '<span class="icon icon--error"></span> Invalid email';
statusEl.className = 'form-field-verification is-invalid';
input.classList.add('is-invalid');
} else if (data.result?.disposable) {
statusEl.innerHTML = '<span class="icon icon--warning"></span> Please use a permanent email';
statusEl.className = 'form-field-verification is-invalid';
input.classList.add('is-invalid');
} else {
this.clearStatus(statusEl);
}
}
showLoading(statusEl) {
statusEl.innerHTML = '<span class="spinner"></span> Verifying...';
statusEl.className = 'form-field-verification is-loading';
}
clearStatus(statusEl) {
statusEl.innerHTML = '';
statusEl.className = 'form-field-verification';
}
isValidFormat(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}테마에서 초기화
// assets/js/theme/global.js
import EmailVerification from './global/email-verification';
export default class Global extends PageManager {
onReady() {
// ... other initializations
new EmailVerification();
}
}테마 구성
config.json에 추가:
{
"settings": {
"emailverify_enabled": true,
"emailverify_api_key": ""
}
}스키마 설정
관리자 구성을 위해 schema.json에 추가:
[
{
"name": "EmailVerify",
"settings": [
{
"type": "checkbox",
"label": "Enable Email Verification",
"id": "emailverify_enabled"
},
{
"type": "text",
"label": "API Key",
"id": "emailverify_api_key"
}
]
}
]템플릿에 Config 주입
{{!-- templates/layout/base.html --}}
<script>
window.emailverifyConfig = {
enabled: {{settings.emailverify_enabled}},
apiKey: '{{settings.emailverify_api_key}}'
};
</script>방법 3: Storefront API 통합
Storefront API를 사용하는 헤드리스 BigCommerce 스토어용.
검증 서비스
// services/email-verification.ts
interface VerificationResult {
email: string;
status: 'valid' | 'invalid' | 'unknown';
score?: number;
isDisposable: boolean;
isRole: boolean;
}
class EmailVerificationService {
private apiKey: string;
private cache: Map<string, VerificationResult> = new Map();
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async verify(email: string): Promise<VerificationResult> {
// Check cache
const cached = this.cache.get(email);
if (cached) {
return cached;
}
try {
const response = await fetch('https://api.emailverify.ai/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
const result: VerificationResult = {
email,
status: data.status,
score: data.score,
isDisposable: data.result?.disposable ?? false,
isRole: data.result?.role ?? false,
};
this.cache.set(email, result);
return result;
} catch (error) {
console.error('Verification error:', error);
return {
email,
status: 'unknown',
isDisposable: false,
isRole: false,
};
}
}
isValid(result: VerificationResult): boolean {
return result.status === 'valid' && !result.isDisposable;
}
getErrorMessage(result: VerificationResult): string | null {
if (result.status === 'invalid') {
return 'Please enter a valid email address.';
}
if (result.isDisposable) {
return 'Temporary email addresses are not accepted.';
}
return null;
}
}
export const emailVerification = new EmailVerificationService(
process.env.EMAILVERIFY_API_KEY!
);React 결제 컴포넌트
// components/checkout/EmailInput.tsx
import { useState, useCallback } from 'react';
import { emailVerification } from '@/services/email-verification';
import { debounce } from 'lodash';
interface EmailInputProps {
value: string;
onChange: (value: string) => void;
onValidationChange: (isValid: boolean) => void;
}
export function EmailInput({ value, onChange, onValidationChange }: EmailInputProps) {
const [status, setStatus] = useState<'idle' | 'verifying' | 'valid' | 'invalid'>('idle');
const [message, setMessage] = useState('');
const verifyEmail = useCallback(
debounce(async (email: string) => {
if (!email || !email.includes('@')) {
setStatus('idle');
setMessage('');
return;
}
setStatus('verifying');
const result = await emailVerification.verify(email);
if (emailVerification.isValid(result)) {
setStatus('valid');
setMessage('Email verified');
onValidationChange(true);
} else {
setStatus('invalid');
setMessage(emailVerification.getErrorMessage(result) || 'Verification failed');
onValidationChange(false);
}
}, 500),
[onValidationChange]
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
verifyEmail(newValue);
};
return (
<div className="form-field">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={value}
onChange={handleChange}
className={`form-input ${status === 'valid' ? 'is-valid' : ''} ${status === 'invalid' ? 'is-invalid' : ''}`}
/>
{status !== 'idle' && (
<span className={`form-feedback ${status}`}>
{status === 'verifying' && <Spinner />}
{message}
</span>
)}
</div>
);
}Next.js API 라우트
API 키를 보호하기 위한 서버 측 검증:
// pages/api/verify-email.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: 'Email required' });
}
try {
const response = await fetch('https://api.emailverify.ai/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.EMAILVERIFY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
return res.status(200).json({
valid: data.status === 'valid' && !data.result?.disposable,
status: data.status,
disposable: data.result?.disposable ?? false,
});
} catch (error) {
console.error('Verification error:', error);
return res.status(200).json({ valid: true }); // Allow on error
}
}웹훅 통합
주문을 처리하고 이메일을 비동기적으로 검증합니다.
웹훅 핸들러
// api/webhooks/bigcommerce.js
import crypto from 'crypto';
import EmailVerify from '@emailverify/sdk';
const bv = new EmailVerify(process.env.EMAILVERIFY_API_KEY);
export default async function handler(req, res) {
// Verify webhook signature
const signature = req.headers['x-bc-webhook-signature'];
const expectedSignature = crypto
.createHmac('sha256', process.env.BC_WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('base64');
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { scope, data } = req.body;
if (scope === 'store/order/created') {
await handleOrderCreated(data);
}
res.status(200).json({ received: true });
}
async function handleOrderCreated(data) {
const orderId = data.id;
// Fetch order details from BigCommerce
const order = await fetchOrder(orderId);
if (!order.billing_address?.email) {
return;
}
// Verify email
const result = await bv.verify({ email: order.billing_address.email });
// Store verification result
await storeVerificationResult(orderId, result);
// Flag suspicious orders
if (result.status === 'invalid' || result.is_disposable) {
await flagOrder(orderId, {
reason: result.status === 'invalid'
? 'Invalid email address'
: 'Disposable email detected',
verification: result,
});
}
}
async function fetchOrder(orderId) {
const response = await fetch(
`https://api.bigcommerce.com/stores/${process.env.BC_STORE_HASH}/v2/orders/${orderId}`,
{
headers: {
'X-Auth-Token': process.env.BC_ACCESS_TOKEN,
'Accept': 'application/json',
},
}
);
return response.json();
}
async function flagOrder(orderId, data) {
// Add order note or update status
await fetch(
`https://api.bigcommerce.com/stores/${process.env.BC_STORE_HASH}/v2/orders/${orderId}`,
{
method: 'PUT',
headers: {
'X-Auth-Token': process.env.BC_ACCESS_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({
staff_notes: `⚠️ Email Verification Warning: ${data.reason}`,
}),
}
);
}웹훅 등록
async function registerWebhook() {
const response = await fetch(
`https://api.bigcommerce.com/stores/${process.env.BC_STORE_HASH}/v3/hooks`,
{
method: 'POST',
headers: {
'X-Auth-Token': process.env.BC_ACCESS_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({
scope: 'store/order/created',
destination: 'https://yoursite.com/api/webhooks/bigcommerce',
is_active: true,
}),
}
);
return response.json();
}대량 고객 검증
스토어의 기존 고객을 검증합니다.
import EmailVerify from '@emailverify/sdk';
const bv = new EmailVerify(process.env.EMAILVERIFY_API_KEY);
async function verifyAllCustomers() {
const customers = await fetchAllCustomers();
const emails = customers.map(c => c.email);
console.log(`Verifying ${emails.length} customer emails...`);
// Submit bulk job
const job = await bv.verifyBulk(emails);
console.log(`Job ID: ${job.job_id}`);
// Wait for completion
let status;
do {
await sleep(5000);
status = await bv.getBulkJobStatus(job.job_id);
console.log(`Progress: ${status.progress_percent}%`);
} while (status.status !== 'completed');
// Process results
const results = await bv.getBulkJobResults(job.job_id);
const stats = { valid: 0, invalid: 0, disposable: 0 };
for (const result of results.results) {
if (result.status === 'valid') stats.valid++;
else stats.invalid++;
if (result.is_disposable) stats.disposable++;
// Update customer record
const customer = customers.find(c => c.email === result.email);
if (customer) {
await updateCustomerMeta(customer.id, {
email_status: result.status,
email_score: result.score,
});
}
}
console.log('Verification complete:', stats);
return stats;
}
async function fetchAllCustomers() {
const customers = [];
let page = 1;
while (true) {
const response = await fetch(
`https://api.bigcommerce.com/stores/${process.env.BC_STORE_HASH}/v3/customers?page=${page}&limit=250`,
{
headers: {
'X-Auth-Token': process.env.BC_ACCESS_TOKEN,
},
}
);
const data = await response.json();
if (data.data.length === 0) break;
customers.push(...data.data);
page++;
}
return customers;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}모범 사례
1. API 키 보호
클라이언트 측 코드에서 API 키를 노출하지 마세요. 프록시를 사용하세요:
// Server-side proxy
app.post('/api/verify', async (req, res) => {
const { email } = req.body;
const result = await bv.verify({ email });
// Only return necessary data
res.json({
valid: result.status === 'valid',
disposable: result.is_disposable,
});
});2. 결과 캐싱
const cache = new Map();
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
function getCachedResult(email) {
const cached = cache.get(email);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.result;
}
return null;
}
function setCachedResult(email, result) {
cache.set(email, { result, timestamp: Date.now() });
}3. 오류를 우아하게 처리
try {
const result = await verifyEmail(email);
// ... handle result
} catch (error) {
console.error('Verification failed:', error);
// Allow checkout to proceed
return { valid: true };
}