Loyalty.lt
SDKsJavaScript

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 time

2. 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();

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).

EventDescriptionData
status_updateLogin status changed{ status, user?, token? }

Status Values

StatusDescription
pendingWaiting for scan
scannedQR scanned, waiting for confirmation
authenticatedLogin successful
cancelledUser cancelled login
failedLogin failed
expiredSession 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

On this page