The Web SDK’s client-side
refreshEntitlements() method has been deprecated. Entitlement validation must be performed server-side using HMAC-authenticated API requests.When to Refresh
Refresh entitlements in these scenarios:1
After Offer Claimed
When a user claims an offer through the SDK, immediately validate their new entitlements.
Copy
const result = await Encore.presentOffer();
if (result.granted) {
// Refresh entitlements from server
const entitlements = await validateEntitlements(userId);
updateUI(entitlements);
}
2
Checking Verification Status
After a provisional grant, periodically check if it’s been verified by the advertiser.
Copy
// Poll for verification
const isVerified = await checkUntilVerified(userId, transactionId);
3
User Returns to App
When a user returns from the advertiser site or navigates back to your app.
Copy
window.addEventListener('focus', async () => {
const entitlements = await validateEntitlements(userId);
updateUI(entitlements);
});
4
Session Start
When a user starts a new session or logs in.
Copy
app.post('/auth/login', async (req, res) => {
const user = await authenticateUser(req.body);
const entitlements = await validateEntitlements(user.id);
req.session.entitlements = entitlements;
res.json({ user, entitlements });
});
Basic Refresh Pattern
Backend Validation Function
Create a reusable function to validate entitlements:Copy
import crypto from 'crypto';
function generateHmacSignature(privateKey, method, path, body) {
const bodyJson = JSON.stringify(body);
const canonical = `${method.toUpperCase()}|${path}|${bodyJson}`;
const hmac = crypto.createHmac('sha256', privateKey);
hmac.update(canonical);
return hmac.digest('hex');
}
async function validateEntitlements(userId) {
const method = 'POST';
const path = '/encore/v1/entitlements/server';
const body = { user_id: userId };
const signature = generateHmacSignature(
process.env.ENCORE_PRIVATE_API_KEY,
method,
path,
body
);
const timestamp = Math.floor(Date.now() / 1000);
const response = await fetch(`${process.env.ENCORE_BASE_URL}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.ENCORE_PUBLIC_API_KEY,
'X-Signature': signature,
'X-Timestamp': timestamp.toString(),
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Encore API error: ${response.statusText}`);
}
return await response.json();
}
// Refresh endpoint
app.post('/api/refresh-entitlements', async (req, res) => {
try {
const { userId } = req.body;
const entitlements = await validateEntitlements(userId);
res.json(entitlements);
} catch (error) {
console.error('Refresh failed:', error);
res.status(500).json({ error: 'Failed to refresh entitlements' });
}
});
Frontend Refresh
Call your backend’s refresh endpoint:Copy
import { useState } from 'react';
function useRefreshEntitlements() {
const [loading, setLoading] = useState(false);
const refresh = async (userId) => {
setLoading(true);
try {
const response = await fetch('/api/refresh-entitlements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
if (!response.ok) {
throw new Error('Refresh failed');
}
return await response.json();
} catch (error) {
console.error('Failed to refresh:', error);
throw error;
} finally {
setLoading(false);
}
};
return { refresh, loading };
}
// Usage
function MyComponent() {
const { refresh, loading } = useRefreshEntitlements();
const [entitlements, setEntitlements] = useState(null);
const handleRefresh = async () => {
const data = await refresh('user_123');
setEntitlements(data);
};
return (
<button onClick={handleRefresh} disabled={loading}>
{loading ? 'Refreshing...' : 'Refresh Entitlements'}
</button>
);
}
Verification Polling
Check periodically if a provisional entitlement becomes verified:Backend Polling Function
Copy
async function pollForVerification(userId, transactionId, options = {}) {
const maxAttempts = options.maxAttempts || 12; // Default: 12 attempts
const delayMs = options.delayMs || 5000; // Default: 5 seconds
const onProgress = options.onProgress || (() => {});
for (let attempt = 0; attempt < maxAttempts; attempt++) {
// Get fresh entitlements
const entitlements = await validateEntitlements(userId);
// Check if entitlement is now verified
const verified = Object.values(entitlements.verified).find(
ent => ent.transactionId === transactionId
);
if (verified) {
onProgress({ status: 'verified', attempt, entitlement: verified });
return { status: 'verified', entitlement: verified };
}
// Check if still provisional
const provisional = Object.values(entitlements.provisional).find(
ent => ent.transactionId === transactionId
);
if (!provisional) {
// Entitlement disappeared (expired or revoked)
onProgress({ status: 'failed', attempt });
return { status: 'failed' };
}
onProgress({ status: 'pending', attempt, entitlement: provisional });
// Wait before next attempt
if (attempt < maxAttempts - 1) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
// Timeout - still provisional
return { status: 'timeout' };
}
// Endpoint
app.post('/api/poll-verification', async (req, res) => {
const { userId, transactionId } = req.body;
try {
const result = await pollForVerification(userId, transactionId, {
maxAttempts: 12,
delayMs: 5000,
});
res.json(result);
} catch (error) {
console.error('Polling failed:', error);
res.status(500).json({ error: 'Polling failed' });
}
});
Frontend Usage
Copy
async function waitForVerification(userId, transactionId) {
try {
const response = await fetch('/api/poll-verification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, transactionId }),
});
const result = await response.json();
if (result.status === 'verified') {
console.log('Entitlement verified!');
showVerifiedBadge();
return true;
} else if (result.status === 'timeout') {
console.log('Verification still pending');
showPendingMessage();
return false;
} else {
console.log('Verification failed');
showErrorMessage();
return false;
}
} catch (error) {
console.error('Polling error:', error);
return false;
}
}
// Use after offer claimed
const result = await Encore.presentOffer();
if (result.granted && result.entitlement) {
// Show provisional access immediately
unlockFeature();
// Wait for verification (non-blocking)
waitForVerification(userId, result.entitlement.transactionId);
}
Caching Strategies
Time-Based Cache
Cache validation results for a fixed duration:Copy
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function getCachedEntitlements(userId) {
const now = Date.now();
const cached = cache.get(userId);
// Return cached if still valid
if (cached && now - cached.timestamp < CACHE_TTL) {
console.log('Returning cached entitlements');
return cached.data;
}
// Fetch fresh data
console.log('Fetching fresh entitlements');
const entitlements = await validateEntitlements(userId);
// Update cache
cache.set(userId, {
data: entitlements,
timestamp: now,
});
return entitlements;
}
// Endpoint with caching
app.post('/api/validate-entitlements', async (req, res) => {
try {
const { userId, forceRefresh } = req.body;
let entitlements;
if (forceRefresh) {
// Bypass cache
entitlements = await validateEntitlements(userId);
cache.set(userId, { data: entitlements, timestamp: Date.now() });
} else {
// Use cache if available
entitlements = await getCachedEntitlements(userId);
}
res.json(entitlements);
} catch (error) {
res.status(500).json({ error: 'Validation failed' });
}
});
Session-Based Cache
Store entitlements in user session:Copy
import session from 'express-session';
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 30 * 60 * 1000 }, // 30 minutes
}));
// Middleware to load/refresh entitlements
async function ensureEntitlements(req, res, next) {
const now = Date.now();
const cacheAge = now - (req.session.entitlementsTimestamp || 0);
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
// Refresh if not cached or cache is stale
if (!req.session.entitlements || cacheAge > CACHE_TTL) {
try {
req.session.entitlements = await validateEntitlements(req.session.userId);
req.session.entitlementsTimestamp = now;
} catch (error) {
console.error('Failed to load entitlements:', error);
req.session.entitlements = { all: {}, verified: {}, provisional: {} };
}
}
next();
}
// Use middleware on protected routes
app.get('/premium-feature', ensureEntitlements, (req, res) => {
if (req.session.entitlements.all?.freeTrial) {
res.render('premium-feature');
} else {
res.redirect('/upgrade');
}
});
// Force refresh endpoint
app.post('/api/refresh-entitlements', async (req, res) => {
try {
const entitlements = await validateEntitlements(req.session.userId);
req.session.entitlements = entitlements;
req.session.entitlementsTimestamp = Date.now();
res.json(entitlements);
} catch (error) {
res.status(500).json({ error: 'Refresh failed' });
}
});
Automatic Refresh Patterns
On Window Focus
Refresh when user returns to your app:Copy
// Frontend
let lastValidation = Date.now();
const VALIDATION_THRESHOLD = 5 * 60 * 1000; // 5 minutes
window.addEventListener('focus', async () => {
const timeSinceLastValidation = Date.now() - lastValidation;
if (timeSinceLastValidation > VALIDATION_THRESHOLD) {
console.log('Window focused after 5 minutes, refreshing entitlements');
const entitlements = await refreshEntitlements(getCurrentUserId());
updateUI(entitlements);
lastValidation = Date.now();
}
});
After Navigation
Refresh on route changes:Copy
// React Router
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
// Refresh on route change
refreshEntitlements(getCurrentUserId());
}, [location.pathname]);
return <Routes>...</Routes>;
}
Periodic Polling
Poll for updates at regular intervals:Copy
// Frontend - Simple polling
class EntitlementPoller {
constructor(userId, intervalMs = 30000) {
this.userId = userId;
this.intervalMs = intervalMs;
this.timerId = null;
}
async poll() {
try {
const response = await fetch('/api/validate-entitlements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: this.userId }),
});
const entitlements = await response.json();
this.onUpdate?.(entitlements);
} catch (error) {
console.error('Poll failed:', error);
}
}
start(callback) {
this.onUpdate = callback;
this.poll(); // Initial poll
this.timerId = setInterval(() => this.poll(), this.intervalMs);
}
stop() {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
}
}
}
// Usage
const poller = new EntitlementPoller('user_123', 30000);
poller.start((entitlements) => {
console.log('Entitlements updated:', entitlements);
updateUI(entitlements);
});
// Cleanup
window.addEventListener('beforeunload', () => poller.stop());
Best Practices
1. Don’t Over-Refresh
Respect rate limits and reduce unnecessary API calls:Copy
// ✅ Good - Reasonable refresh interval
const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
// ❌ Bad - Too frequent
const REFRESH_INTERVAL = 1000; // 1 second (way too often!)
2. Cache Appropriately
Balance freshness with performance:Copy
// ✅ Good - 5 minute cache with forced refresh option
async function getEntitlements(userId, forceRefresh = false) {
if (!forceRefresh) {
const cached = getCached(userId);
if (cached && isStillValid(cached)) {
return cached.data;
}
}
return await validateEntitlements(userId);
}
3. Handle Errors Gracefully
Don’t break user experience on refresh failures:Copy
// ✅ Good - Graceful error handling
async function refreshWithFallback(userId) {
try {
return await validateEntitlements(userId);
} catch (error) {
console.error('Refresh failed, using cached data:', error);
return getCachedOrDefault(userId);
}
}
4. Provide User Feedback
Show loading states during refresh:Copy
// ✅ Good - Loading state
async function handleRefresh() {
setLoading(true);
try {
const entitlements = await refreshEntitlements(userId);
setEntitlements(entitlements);
showMessage('Entitlements updated');
} catch (error) {
showError('Failed to refresh');
} finally {
setLoading(false);
}
}
5. Refresh After Key Actions
Refresh after actions that might change entitlements:Copy
// ✅ Good - Refresh after offer claimed
async function handleOfferClaimed() {
const result = await Encore.presentOffer();
if (result.granted) {
// Immediately refresh to get new entitlements
const entitlements = await refreshEntitlements(userId);
updateUI(entitlements);
}
}
Complete Example
Here’s a complete implementation with caching and multiple refresh triggers:Copy
// Backend
import express from 'express';
import crypto from 'crypto';
const app = express();
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000;
function generateHmacSignature(privateKey, method, path, body) {
const bodyJson = JSON.stringify(body);
const canonical = `${method.toUpperCase()}|${path}|${bodyJson}`;
const hmac = crypto.createHmac('sha256', privateKey);
hmac.update(canonical);
return hmac.digest('hex');
}
async function validateEntitlements(userId) {
const method = 'POST';
const path = '/encore/v1/entitlements/server';
const body = { user_id: userId };
const signature = generateHmacSignature(
process.env.ENCORE_PRIVATE_API_KEY,
method,
path,
body
);
const timestamp = Math.floor(Date.now() / 1000);
const response = await fetch(`${process.env.ENCORE_BASE_URL}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.ENCORE_PUBLIC_API_KEY,
'X-Signature': signature,
'X-Timestamp': timestamp.toString(),
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Encore API error: ${response.statusText}`);
}
return await response.json();
}
app.post('/api/validate-entitlements', async (req, res) => {
try {
const { userId, forceRefresh } = req.body;
const now = Date.now();
const cached = cache.get(userId);
// Use cache unless forced refresh or stale
if (!forceRefresh && cached && now - cached.timestamp < CACHE_TTL) {
return res.json(cached.data);
}
// Fetch fresh data
const entitlements = await validateEntitlements(userId);
cache.set(userId, { data: entitlements, timestamp: now });
res.json(entitlements);
} catch (error) {
console.error('Validation error:', error);
res.status(500).json({ error: 'Validation failed' });
}
});
app.listen(3000);
Copy
// Frontend - React
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
const EntitlementContext = createContext(null);
export function EntitlementProvider({ children, userId }) {
const [entitlements, setEntitlements] = useState(null);
const [loading, setLoading] = useState(true);
const refresh = useCallback(async (forceRefresh = false) => {
setLoading(true);
try {
const response = await fetch('/api/validate-entitlements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, forceRefresh }),
});
const data = await response.json();
setEntitlements(data);
} catch (error) {
console.error('Failed to refresh:', error);
} finally {
setLoading(false);
}
}, [userId]);
// Initial load
useEffect(() => {
refresh();
}, [refresh]);
// Refresh on window focus (if stale)
useEffect(() => {
let lastRefresh = Date.now();
const handleFocus = () => {
if (Date.now() - lastRefresh > 5 * 60 * 1000) {
refresh(true);
lastRefresh = Date.now();
}
};
window.addEventListener('focus', handleFocus);
return () => window.removeEventListener('focus', handleFocus);
}, [refresh]);
return (
<EntitlementContext.Provider value={{ entitlements, loading, refresh }}>
{children}
</EntitlementContext.Provider>
);
}
export function useEntitlements() {
return useContext(EntitlementContext);
}
Migration from Client-Side Method
If you were using the deprecated client-side method:Copy
// ❌ Old - Client-side refresh (deprecated)
import Encore from '@encore/web-sdk';
await Encore.refreshEntitlements();
// ✅ New - Server-side refresh
const response = await fetch('/api/validate-entitlements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: getCurrentUserId(),
forceRefresh: true
}),
});
const entitlements = await response.json();
Related Documentation
- Server-Side Validation - HMAC authentication details
- Managing State - State management patterns
- Track Entitlements Guide - Complete validation guide
Next Steps
- Server-Side Validation - Learn HMAC-based validation
- Managing State - State management patterns