QR Login
Implement QR code-based authentication with the SDK
QR Login
QR Login allows customers to authenticate on web/POS systems by scanning a QR code with their Loyalty.lt mobile app.
How It Works
Generate QR Session
Your system generates a QR code with a unique session ID
Display QR Code
Customer sees the QR code on your website/POS display
Customer Scans
Customer scans the QR code with Loyalty.lt mobile app
Confirm Login
Customer confirms the login in their app
Receive Authentication
Your system receives the authentication token via real-time events
Implementation
1. Generate QR Login Session
const session = await sdk.generateQrLogin('Web Browser');
console.log(session.session_id); // Unique session ID
console.log(session.qr_code); // Deep link for QR code
console.log(session.expires_at); // Expiration time2. Display QR Code
// Generate QR code image URL using Loyalty.lt API
const qrImageUrl = `https://api.loyalty.lt/qr?data=${encodeURIComponent(session.qr_code)}&size=250`;
// Or use qrcode.react for client-side rendering
<QRCodeSVG value={session.qr_code} size={250} />3. Subscribe to Real-time Events
The simplest option is the SDK helper, which connects to Reverb for you:
const unsubscribe = sdk.subscribeToQrLogin(session.session_id, (payload) => {
console.log('Status:', payload.status);
});If you prefer to drive the connection yourself, use pusher-js directly:
// 1. Fetch Reverb connection details
const config = await sdk.getRealtimeConfig();
// 2. Connect to Reverb (Pusher protocol)
const pusher = new Pusher(config.key, {
wsHost: config.host,
wsPort: config.port,
wssPort: config.port,
forceTLS: config.scheme === 'https',
enabledTransports: ['ws', 'wss'],
cluster: '', // Reverb has no clusters, but pusher-js throws without this option
disableStats: true
});
// 3. Subscribe to the public session channel
const channel = pusher.subscribe(`${config.channel_prefix.qr_login}${session.session_id}`);
// 4. Listen for status updates
channel.bind(config.event, (payload) => {
const { status, user, token } = payload;
switch (status) {
case 'scanned':
showMessage('QR scanned! Please confirm on your phone...');
break;
case 'authenticated':
showMessage('Login successful!');
handleLogin(user, token);
break;
case 'cancelled':
showMessage('Login cancelled');
regenerateQR();
break;
case 'failed':
showMessage('Login failed');
regenerateQR();
break;
}
});Note
There are no realtime tokens anymore. The QR login channel (qr-login.{session_id}) is a
public channel — the unguessable session ID is the secret — and pusher-js reconnects
automatically, so nothing has to be refreshed. Connection details come from
GET /{locale}/shop/realtime/config.
4. Polling Fallback
If WebSocket is unavailable, use polling:
async function pollLoginStatus(sessionId: string) {
const interval = setInterval(async () => {
try {
const status = await sdk.pollQrLoginStatus(sessionId);
if (status.status === 'authenticated' && status.token) {
clearInterval(interval);
handleLogin(status.user, status.token);
} else if (status.status === 'expired') {
clearInterval(interval);
regenerateQR();
}
} catch (error) {
console.error('Polling error:', error);
}
}, 2000);
// Stop polling after 5 minutes
setTimeout(() => clearInterval(interval), 5 * 60 * 1000);
}Complete Example
const sdk = new LoyaltySDK({
apiKey: 'lty_...',
apiSecret: '...',
environment: 'production'
});
let pusherClient: Pusher | null = null;
let currentSession: any = null;
async function startQRLogin() {
// Generate session
currentSession = await sdk.generateQrLogin('Web Browser');
// Display QR code
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${encodeURIComponent(currentSession.qr_code)}`;
document.getElementById('qr-image').src = qrUrl;
// Fetch Reverb connection details
const config = await sdk.getRealtimeConfig();
pusherClient = new Pusher(config.key, {
wsHost: config.host,
wsPort: config.port,
wssPort: config.port,
forceTLS: config.scheme === 'https',
enabledTransports: ['ws', 'wss'],
cluster: '', // Reverb has no clusters, but pusher-js throws without this option
disableStats: true
});
const channel = pusherClient.subscribe(
`${config.channel_prefix.qr_login}${currentSession.session_id}`
);
channel.bind(config.event, handleStatusUpdate);
}
function handleStatusUpdate(payload: any) {
const { status, user, token } = payload;
if (status === 'authenticated') {
// Store token, redirect user, etc.
localStorage.setItem('auth_token', token);
window.location.href = '/dashboard';
}
}
// Start QR login when page loads
startQRLogin();Send App Download Link
If the customer doesn't have the app:
await sdk.sendAppLink(
'+37060000000', // Phone number
shopId, // Shop ID (required)
'John', // Optional customer name
'lt' // Language (lt/en)
);React Component
For React applications, use the pre-built component:
function LoginPage() {
return (
<QRLogin
apiKey="lty_..."
apiSecret="..."
onAuthenticated={(user, token) => {
console.log('Logged in:', user);
// Handle successful login
}}
onError={(error) => {
console.error('Login error:', error);
}}
/>
);
}See React Components for more details.
Reverb Channel Events
Channel: qr-login.{session_id} (public — no channel authentication needed).
| Event | Description | Data |
|---|---|---|
status_update | Login status changed | { status, user?, token? } |
Status Values
| Status | Description |
|---|---|
pending | Waiting for scan |
scanned | QR scanned, waiting for confirmation |
authenticated | Login successful |
cancelled | User cancelled login |
failed | Login failed |
expired | Session expired |
Security Considerations
- QR sessions expire after 5 minutes
- Each session can only be used once
- Always validate tokens on your backend
- Use HTTPS for all communications
Next Steps
Customer identification for POS
Pre-built UI components