luqta_sdk 1.5.0 copy "luqta_sdk: ^1.5.0" to clipboard
luqta_sdk: ^1.5.0 copied to clipboard

Official Luqta Flutter SDK for contests, quizzes, rewards & gamification. Preconfigured UI or custom API mode. Level/contest availability checks, profile icon, auto token refresh, and full RTL support.

Luqta Flutter SDK #

pub.dev pub points License: MIT Platform

Official Flutter SDK for the Luqta platform — integrate contests, quizzes, rewards, and gamification into your Flutter app in minutes.

Version: 1.5.0


Table of Contents #


Features #

  • Two Integration Modes — Preconfigured (instant full UI) or Custom (API-only, bring your own UI)
  • Contest Management — Browse, participate, track progress, private contest access codes
  • Level Completion — Text, QR code, Link, Image, Quiz, Client Webhook, Luqta Webhook
  • Availability Checks — Automatic level/contest start/end date gating with user-facing alerts
  • Smart Date Parsing — Handles both ISO-8601 strings and numeric epoch timestamps (ms or s)
  • Profile Icon — Optional draggable floating profile button with full stats bottom sheet
  • Auto Token Refresh — Expired tokens are silently refreshed and requests retried once
  • Auto User Sync — First-time users are auto-synced if userProfile is provided in config
  • Zero Storage Mode — No PII stored; all identifiers hashed server-side with SHA-256
  • Multi-language — English and Arabic built-in; full RTL layout support
  • Beautiful Pre-built UI — Contest carousel, detail screens, shimmer loading, toast notifications
  • Secure — Rate limiting, request deduplication, secure token storage

Installation #

Add to your pubspec.yaml:

dependencies:
  luqta_sdk: ^1.5.0

Then run:

flutter pub get

Import in your Dart files:

import 'package:luqta_sdk/luqta_sdk.dart';

Quick Start #

Minimum Setup #

// 1. Create client
final client = LuqtaClient(
  config: LuqtaConfig(
    apiKey: 'your-api-key',
    appId:  'your-app-id',
  ),
);

// 2. Initialize SDK
await client.initializeSdk();

// 3. Initialize user (first-time: sync + initialize in one call)
await client.syncAndInitializeUser(UserProfile(
  name:         'John Doe',
  email:        'john@example.com',
  policyAccept: true,
));

// Done — use client.contests, client.levels, etc.

Usage Modes #

Mode Best For Effort
Preconfigured Quick integration, standard UI ~10 lines
Custom Full UI control, custom designs More work

1. Preconfigured Mode (Easiest) #

One call — render() — returns a complete, production-ready Widget tree. The SDK handles all screens, navigation, data fetching, and state management internally.

import 'package:flutter/material.dart';
import 'package:luqta_sdk/luqta_sdk.dart';

class LuqtaPage extends StatefulWidget {
  const LuqtaPage({super.key});
  @override
  State<LuqtaPage> createState() => _LuqtaPageState();
}

class _LuqtaPageState extends State<LuqtaPage> {
  LuqtaClient? _client;
  bool _ready = false;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    _client = LuqtaClient(
      config: LuqtaConfig(
        mode:       LuqtaMode.preconfigured,
        apiKey:     'your-api-key',
        appId:      'your-app-id',
        production: false,           // set true for production
        locale:     'en',            // 'en' or 'ar'
        rtl:        false,
        branding: BrandingConfig(
          primaryColor: Color(0xFF5304FB),
        ),
        // Optional: show floating profile button
        showProfileIcon: true,
        // Optional: auto-sync first-time users
        userProfile: UserProfile(
          name:         'John Doe',
          email:        'john@example.com',
          policyAccept: true,
        ),
        onAction: (action) => debugPrint('Action: ${action['type']}'),
        onError:  (error)  => debugPrint('Error: $error'),
      ),
    );

    await _client!.initializeSdk();
    await _client!.syncAndInitializeUser(UserProfile(
      email:        'john@example.com',
      policyAccept: true,
    ));

    setState(() => _ready = true);
  }

  @override
  Widget build(BuildContext context) {
    if (!_ready) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }
    return Scaffold(body: _client!.render());
  }
}

What render() gives you #

  • Contest carousel with horizontal swipe
  • Contest detail pages with level list
  • Level completion flows (text, QR, link, image, quiz, webhook)
  • Private contest access-code bottom sheet
  • Progress tracking and congratulation dialogs
  • Level/contest availability alerts (not started yet, already ended)
  • Draggable profile icon with stats sheet (if showProfileIcon: true)
  • All navigation and animations

2. Custom Mode (Full Control) #

Use the SDK as a pure API layer and build your own UI.

Setup #

final client = LuqtaClient(
  config: LuqtaConfig(
    apiKey: 'your-api-key',
    appId:  'your-app-id',
  ),
);

await client.initializeSdk();
await client.syncAndInitializeUser(UserProfile(
  email:        'user@example.com',
  policyAccept: true,
));

API Services #

// Contests
final response = await client.contests.getAll();
final contests = response.data.items
    .map((e) => Contest.fromJson(e))
    .toList();

await client.contests.participate(123);
await client.contests.participate(123, accessCode: 'PRIV-CODE');
await client.contests.compete(123);  // mark completed

// Levels
await client.levels.complete(456, data: {'textContent': 'my answer'});
await client.levels.complete(456, data: {'qrCode': 'scanned-value'});
await client.levels.complete(456, data: {'link': 'https://example.com'});
await client.levels.completeWithImage(456, 'https://cdn.example.com/photo.jpg');
await client.levels.completeClientWebhook(456, variables: [
  {'variable_name': 'email', 'value': 'user@example.com'},
]);

// Quiz
final quiz = await client.quiz.start(quizId, levelId);
final result = await client.quiz.submitAnswer(attemptId, questionId, optionId, levelId);

// Profile
final profile = await client.profile.get();

// Rewards
final rewards = await client.rewards.getAll();

// Notifications
final notifications = await client.notifications.getAll();
await client.notifications.markAsRead(notificationId);

Using individual widgets #

// Ready-made contest list screen
ContestsScreen(
  locale: 'en',
  showSeeAllButton: true,
  onFetchContests: ({required int page, required int limit}) async {
    final res = await client.contests.getAll();
    return res.data.items.map((e) => Contest.fromJson(e)).toList();
  },
  onContestTap: (contest) => _openDetail(contest),
)

// Contest detail screen
ContestDetailScreen(
  contest: contest,
  locale:  'en',
  onBack:  () => Navigator.pop(context),
  onFetchContestDetails: (id) async {
    final res = await client.getContestDetailsProgress(id);
    return Contest.fromJson(res['data']);
  },
  onJoinContest: (id, accessCode) async {
    await client.contests.participate(id, accessCode: accessCode);
    final res = await client.getContestDetailsProgress(id);
    return Contest.fromJson(res['data']);
  },
  onCompleteLevel: (levelId, type, {
    textContent, qrCode, link, imageUrl, webhookPayload,
  }) async {
    final res = await client.levels.complete(levelId, data: {
      if (textContent != null) 'textContent': textContent,
      if (qrCode     != null) 'qrCode':       qrCode,
      if (link       != null) 'link':          link,
    });
    return res['success'] == true;
  },
)

Configuration #

LuqtaConfig Options #

Parameter Type Default Description
apiKey String required Your Luqta API key
appId String required Your application ID
mode LuqtaMode custom custom or preconfigured
production bool false Use production endpoint
user LuqtaUser? null User identification
userProfile UserProfile? null Full profile for auto-sync on first run
branding BrandingConfig? null UI branding (preconfigured mode)
locale String 'en' 'en' or 'ar'
rtl bool false Right-to-left layout
showProfileIcon bool false Show draggable profile button
zeroStorage bool false Hash all PII server-side
timeout Duration 30s HTTP request timeout
headers Map<String, String>? null Extra HTTP headers
onAction Function? null Action event callback
onError Function? null Error event callback

User Identification #

At least one identifier is required. Priority for hash lookup: uuidusernameemailphoneNumber.

// by email
LuqtaUser(email: 'user@example.com')

// by phone (international format)
LuqtaUser(phoneNumber: '+923001234567')

// by username
LuqtaUser(username: 'john_doe')

// by UUID (v4)
LuqtaUser(uuid: '550e8400-e29b-41d4-a716-446655440000')

// multiple identifiers
LuqtaUser(
  email:    'user@example.com',
  username: 'john_doe',
  uuid:     '550e8400-e29b-41d4-a716-446655440000',
)

Branding #

BrandingConfig(
  primaryColor:   Color(0xFF5304FB),
  secondaryColor: Color(0xFF8F67FD),
  backgroundColor: Colors.white,
  textColor:       Colors.black87,
  borderRadius:    12.0,
  fontFamily:      'Poppins, sans-serif',
)

Localization & RTL #

LuqtaConfig(
  locale: 'ar',  // Arabic
  rtl:    true,  // right-to-left layout
)

Use translations in custom UI:

final label = LuqtaTranslations.translate('contests.title', locale: 'ar');
// → "المسابقات"

Zero Storage Mode #

When enabled, the backend hashes every PII field using SHA-256 — no plaintext is ever written to any column.

LuqtaConfig(
  apiKey:      'your-api-key',
  appId:       'your-app-id',
  zeroStorage: true,
)

await client.syncAndInitializeUser(UserProfile(
  email: 'user@example.com',
  name:  'John Doe',
));
// → Only sdk_hash_user is stored; all PII columns remain null.

Re-syncing with zeroStorage: true automatically clears any previously stored plaintext PII.


Profile Icon #

When showProfileIcon: true and the user is initialized, a draggable circular button appears (bottom-right corner). Tapping it opens a bottom sheet showing the user's name, contact info, total points, and contest statistics.

LuqtaConfig(
  showProfileIcon: true,
  // other options …
)

The icon is only rendered in preconfigured mode (render()). In custom mode, call client.profile.get() directly and build your own profile UI.


API Reference #

Client Methods #

Method Description
initializeSdk() Obtain SDK-level auth token
initializeUser() Obtain user-level auth token (requires prior syncUser)
syncUser(profile) Push user data to backend
syncAndInitializeUser(profile) Sync and initialize in one call (recommended for first-time users)
tryRestoreSession() Rehydrate tokens from secure storage (called automatically)
isInitialized() Returns true when user token is active
hasUserToken() Async check for persisted user token
render() Returns pre-built UI Widget (preconfigured mode only)
getContestDetailsProgress(id) Fetch contest with user progress (dual-auth endpoint)
getContestDetailsApp(id) Fetch contest details (SDK-only endpoint)
post(endpoint, body) Low-level authenticated POST

API Services #

// client.contests
client.contests.getAll()
client.contests.participate(id, {accessCode})
client.contests.compete(id)

// client.levels
client.levels.complete(levelId, data: {...})
client.levels.completeWithImage(levelId, imageUrl)
client.levels.completeClientWebhook(levelId, variables: [...])

// client.quiz
client.quiz.start(quizId, levelId)
client.quiz.submitAnswer(attemptId, questionId, optionId, levelId)

// client.profile
client.profile.get()

// client.rewards
client.rewards.getAll()
client.rewards.redeem(rewardId)

// client.notifications
client.notifications.getAll()
client.notifications.markAsRead(id)

UserProfile Fields #

UserProfile(
  email:        'user@example.com',
  phoneNumber:  '+923001234567',
  name:         'John Doe',
  username:     'john_doe',       // → sdk_username
  userId:       'ext-123',        // → sdk_user_id
  uuid:         '550e8400-...',
  country:      'PK',
  gender:       'male',
  dob:          '1990-01-01',
  verified:     true,
  policyAccept: true,
  interestedIn: ['sports', 'tech'],
  imageUrl:     'https://cdn.example.com/avatar.jpg',
)

Level Types #

levelType Completion data Description
text {'textContent': '...'} Free-text answer
qr {'qrCode': '...'} Scanned QR value
link {'link': 'https://...'} URL submission
image imageUrl (use completeWithImage) Photo upload
quiz Handled by quiz flow Interactive quiz
client_webhook variables: [...] (use completeClientWebhook) Custom form POSTed to client URL
luqta_webhook Automatically handled Opens Luqta-hosted URL

Date Handling #

The SDK handles dates returned by the backend in either format:

Format Example
ISO-8601 string "2026-05-01T12:00:00.000Z"
Epoch milliseconds (Number) 1746388800000
Epoch seconds (Number) 1746388800

All date fields on Level and Contest are stored as String? internally. Use LevelDateParser (or the built-in availability checks) to work with them:

// Check whether a level is currently available
final available = LevelDateParser.isLevelAvailable(level);

// Parse a raw date string to DateTime
final dt = LevelDateParser.parseDate(level.endDate);

Availability Checks #

The SDK automatically evaluates startDate, endDate, and levelTimeline for both levels and contests. When a level has not started yet or has already ended, the pre-built UI shows a localized alert dialog instead of the action button. Contest detail screens similarly gate access when a contest is not active.

In custom mode, check availability manually using the date fields on the model:

// Parse a raw date string/epoch to DateTime
DateTime? parseDate(String? raw) {
  if (raw == null || raw.isEmpty) return null;
  final ms = int.tryParse(raw);
  if (ms != null) {
    // Distinguish milliseconds (13-digit) from seconds (10-digit)
    return ms > 1e11
        ? DateTime.fromMillisecondsSinceEpoch(ms)
        : DateTime.fromMillisecondsSinceEpoch(ms * 1000);
  }
  return DateTime.tryParse(raw);
}

// Level availability
final now   = DateTime.now();
final start = parseDate(level.startDate);
final end   = parseDate(level.endDate ?? level.levelTimeline);

if (start != null && now.isBefore(start)) {
  showDialog(/* Level has not started yet */);
} else if (end != null && now.isAfter(end)) {
  showDialog(/* Level has ended */);
}

// Contest availability
if (contest.status == 'completed') {
  // show "contest ended" message
}

Error Handling #

Try / catch #

try {
  await client.contests.participate(contestId);
} on LuqtaError catch (e) {
  print('${e.code}: ${e.message} (HTTP ${e.status})');
} catch (e) {
  print('Unexpected error: $e');
}

onError callback #

LuqtaConfig(
  onError: (error) {
    if (error is LuqtaError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(error.message)),
      );
    }
  },
)

Token expiry #

The SDK detects expired tokens (ERROR_FATAL + "token expired") and silently re-initializes before retrying the failed request once. No action required in your code.

Common Error Codes #

Code Description
MISSING_API_KEY API key not provided
MISSING_APP_ID App ID not provided
INVALID_EMAIL_FORMAT Invalid email format
INVALID_PHONE_FORMAT Phone not in international format
RATE_LIMIT_EXCEEDED Too many requests
REQUEST_FAILED HTTP request failed
SDK-USER-001 User must be synced before initializing
ERROR_FATAL Fatal server-side error (often token expiry)

Complete Examples #

Example 1: Minimal preconfigured #

import 'package:flutter/material.dart';
import 'package:luqta_sdk/luqta_sdk.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) => const MaterialApp(home: LuqtaPage());
}

class LuqtaPage extends StatefulWidget {
  const LuqtaPage({super.key});
  @override
  State<LuqtaPage> createState() => _LuqtaPageState();
}

class _LuqtaPageState extends State<LuqtaPage> {
  LuqtaClient? _client;
  bool _ready = false;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    _client = LuqtaClient(
      config: LuqtaConfig(
        mode:   LuqtaMode.preconfigured,
        apiKey: 'YOUR_API_KEY',
        appId:  'YOUR_APP_ID',
      ),
    );
    await _client!.initializeSdk();
    await _client!.syncAndInitializeUser(
      UserProfile(email: 'user@example.com', policyAccept: true),
    );
    setState(() => _ready = true);
  }

  @override
  Widget build(BuildContext context) {
    if (!_ready) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }
    return Scaffold(body: _client!.render());
  }
}

Example 2: Embed in existing app #

class HomePage extends StatelessWidget {
  final LuqtaClient client;
  const HomePage({super.key, required this.client});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('My App')),
      body: Column(
        children: [
          const MyCustomHeader(),
          SizedBox(
            height: 320,
            child: client.render(),   // Luqta contests section
          ),
          const MyOtherContent(),
        ],
      ),
    );
  }
}

Example 3: Custom UI — full control #

class CustomContestsPage extends StatelessWidget {
  final LuqtaClient client;
  const CustomContestsPage({super.key, required this.client});

  @override
  Widget build(BuildContext context) {
    return ContestsScreen(
      locale:          'en',
      showSeeAllButton: true,
      onFetchContests: ({required int page, required int limit}) async {
        final res = await client.contests.getAll();
        return res.data.items.map((e) => Contest.fromJson(e)).toList();
      },
      onContestTap: (contest) => Navigator.push(
        context,
        MaterialPageRoute(
          builder: (_) => ContestDetailScreen(
            contest: contest,
            locale:  'en',
            onBack:  () => Navigator.pop(context),
            onFetchContestDetails: (id) async {
              final res = await client.getContestDetailsProgress(id);
              return Contest.fromJson(res['data']);
            },
            onCompleteLevel: (levelId, type,
                {textContent, qrCode, link, imageUrl, webhookPayload}) async {
              final res = await client.levels.complete(levelId, data: {
                if (textContent != null) 'textContent': textContent,
                if (qrCode      != null) 'qrCode':      qrCode,
                if (link        != null) 'link':         link,
              });
              return res['success'] == true;
            },
            onJoinContest: (id, code) async {
              await client.contests.participate(id, accessCode: code);
              final res = await client.getContestDetailsProgress(id);
              return Contest.fromJson(res['data']);
            },
          ),
        ),
      ),
    );
  }
}

Available Widgets #

Screens #

Widget Description
ContestsScreen Full contest list (horizontal carousel or vertical)
ContestDetailScreen Contest detail with levels, progress, prizes

Reusable Widgets #

Widget Description
ContestCard Contest card for custom lists
LevelItemView Level row with icon, status, points
QuizWidget Interactive quiz with answer flow
CongratulationDialog Level/contest completion celebration
LuqtaToast Toast notifications (renders above bottom sheets)

Level Completion Widgets #

Widget Description
TextLevelView Free-text answer input
QRLevelView Camera QR code scanner
LinkLevelView URL submission
ImageLevelView Image capture / upload

Support #


License #

MIT License — see LICENSE for details.

0
likes
110
points
36
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Official Luqta Flutter SDK for contests, quizzes, rewards & gamification. Preconfigured UI or custom API mode. Level/contest availability checks, profile icon, auto token refresh, and full RTL support.

Homepage
Repository (GitHub)
View/report issues

Topics

#gamification #contests #quizzes #rewards #engagement

License

MIT (license)

Dependencies

cached_network_image, flutter, flutter_secure_storage, http, image_picker, intl, mobile_scanner, provider, shared_preferences, shimmer, url_launcher

More

Packages that depend on luqta_sdk