rapidoreach 1.1.0 copy "rapidoreach: ^1.1.0" to clipboard
rapidoreach: ^1.1.0 copied to clipboard

Monetize your users through rewarded surveys!

example/lib/main.dart

import 'dart:convert';
import 'dart:io';

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

enum DemoTab { dashboard, rewards, logs }

class LogEntry {
  final String id;
  final int timestampMs;
  final String level; // info | event | error
  final String title;
  final String? request;
  final String? response;

  LogEntry({
    required this.id,
    required this.timestampMs,
    required this.level,
    required this.title,
    this.request,
    this.response,
  });
}

class RewardEntry {
  final String id;
  final int timestampMs;
  final int amount;
  final String placement;
  final String note;
  final String source;

  RewardEntry({
    required this.id,
    required this.timestampMs,
    required this.amount,
    required this.placement,
    required this.note,
    required this.source,
  });
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  DemoTab _tab = DemoTab.dashboard;

  String _apiKey = '';
  String _userId = Platform.isIOS ? 'DEMO_USER_ID' : 'ANDROID_TEST_ID';
  String _placementTag = 'default';
  String? _baseUrl;

  late final TextEditingController _apiKeyController;
  late final TextEditingController _userIdController;
  late final TextEditingController _placementController;
  late final TextEditingController _customParamsController;
  late final TextEditingController _attributesController;
  late final TextEditingController _questionIdController;
  late final TextEditingController _questionAnswerController;

  bool _sdkInitialized = false;
  bool? _surveyAvailable;
  String? _lastSurveyId;

  bool _usingAltTheme = false;
  String _attributesJson =
      const JsonEncoder.withIndent('  ').convert(<String, dynamic>{
    'qa_timestamp': null, // filled in initState
    'qa_flag': 'default',
  });
  String _customParamsJson =
      const JsonEncoder.withIndent('  ').convert(<String, dynamic>{'qa': true});

  String _questionId = '';
  String _questionAnswer = 'yes';

  final List<LogEntry> _logs = <LogEntry>[];
  final List<RewardEntry> _rewards = <RewardEntry>[];
  int _idCounter = 0;

  String _makeId() => '${DateTime.now().millisecondsSinceEpoch}-${_idCounter++}';

  int get _rewardTotal =>
      _rewards.fold<int>(0, (sum, r) => sum + r.amount);

  String _formatTimestamp(int ms) =>
      DateTime.fromMillisecondsSinceEpoch(ms).toLocal().toString();

  void _addLog({
    required String level,
    required String title,
    String? request,
    String? response,
  }) {
    setState(() {
      _logs.insert(
        0,
        LogEntry(
          id: _makeId(),
          timestampMs: DateTime.now().millisecondsSinceEpoch,
          level: level,
          title: title,
          request: request,
          response: response,
        ),
      );
    });
  }

  void _addReward({
    required int amount,
    required String placement,
    required String note,
    required String source,
  }) {
    setState(() {
      _rewards.insert(
        0,
        RewardEntry(
          id: _makeId(),
          timestampMs: DateTime.now().millisecondsSinceEpoch,
          amount: amount,
          placement: placement,
          note: note,
          source: source,
        ),
      );
    });
  }

  Map<String, dynamic> _tryParseJsonObject(String text) {
    final trimmed = text.trim();
    if (trimmed.isEmpty) return <String, dynamic>{};
    final decoded = jsonDecode(trimmed);
    if (decoded is Map) return Map<String, dynamic>.from(decoded);
    return <String, dynamic>{};
  }

  Future<void> _applyTheme(bool alt) async {
    if (alt) {
      await RapidoReach.instance.setNavBarColor(color: '#FF7043');
      await RapidoReach.instance.setNavBarTextColor(textColor: '#000000');
      await RapidoReach.instance.setNavBarText(text: 'QA Theme');
    } else {
      await RapidoReach.instance.setNavBarColor(color: '#211548');
      await RapidoReach.instance.setNavBarTextColor(textColor: '#FFFFFF');
      await RapidoReach.instance.setNavBarText(text: 'RapidoReach');
    }
  }

  Future<void> _initialize() async {
    final key = _apiKey.trim();
    final uid = _userId.trim();
    if (key.isEmpty || uid.isEmpty) {
      _showSnack('Enter an API key and user id first.');
      return;
    }

    _addLog(
      level: 'info',
      title: 'Initialize SDK',
      request:
          'init(apiKey: ${key.length > 6 ? '${key.substring(0, 6)}…' : key}, userId: $uid)',
    );

    try {
      await RapidoReach.instance.init(apiToken: key, userId: uid);
      await _applyTheme(_usingAltTheme);
      setState(() {
        _sdkInitialized = true;
      });
      _addLog(level: 'info', title: 'SDK initialized');

      final available = await RapidoReach.instance.isSurveyAvailable();
      setState(() {
        _surveyAvailable = available;
      });
    } catch (e) {
      _addLog(level: 'error', title: 'Initialization failed', response: '$e');
      _showSnack('Init failed: $e');
    }
  }

  Future<void> _updateUserIdentifier() async {
    final uid = _userId.trim();
    if (uid.isEmpty) {
      _showSnack('Enter a user id first.');
      return;
    }
    _addLog(level: 'info', title: 'setUserIdentifier', request: uid);
    try {
      await RapidoReach.instance.setUserIdentifier(userId: uid);
      _addLog(level: 'info', title: 'User identifier updated');
    } catch (e) {
      _addLog(
          level: 'error', title: 'setUserIdentifier failed', response: '$e');
      _showSnack('setUserIdentifier failed: $e');
    }
  }

  Future<void> _openOfferwall() async {
    _addLog(level: 'info', title: 'showRewardCenter');
    try {
      await RapidoReach.instance.showRewardCenter();
    } catch (e) {
      _addLog(level: 'error', title: 'showRewardCenter failed', response: '$e');
      _showSnack('showRewardCenter failed: $e');
    }
  }

  Future<void> _checkPlacement() async {
    final tag = _placementTag.trim();
    if (tag.isEmpty) {
      _showSnack('Enter a placement tag first.');
      return;
    }
    _addLog(
      level: 'info',
      title: 'Check Placement',
      request: 'canShowContent(tag: $tag)',
    );
    try {
      final canShow = await RapidoReach.instance.canShowContent(tag: tag);
      _addLog(
          level: 'info',
          title: canShow ? 'Placement ready' : 'Placement not ready');
      _showSnack(canShow ? 'Placement ready' : 'Placement not ready');
    } catch (e) {
      _addLog(level: 'error', title: 'Check Placement failed', response: '$e');
      _showSnack('Check Placement failed: $e');
    }
  }

  Future<void> _getPlacementDetails() async {
    final tag = _placementTag.trim();
    if (tag.isEmpty) {
      _showSnack('Enter a placement tag first.');
      return;
    }
    _addLog(
      level: 'info',
      title: 'Get Placement Details',
      request: 'getPlacementDetails(tag: $tag)',
    );
    try {
      final details = await RapidoReach.instance.getPlacementDetails(tag: tag);
      final pretty = const JsonEncoder.withIndent('  ').convert(details);
      _addLog(level: 'info', title: 'Placement details received', response: pretty);
      await _showDialog('Placement details', pretty);
    } catch (e) {
      _addLog(
          level: 'error', title: 'Get Placement Details failed', response: '$e');
      _showSnack('Get Placement Details failed: $e');
    }
  }

  Future<void> _listSurveys() async {
    final tag = _placementTag.trim();
    if (tag.isEmpty) {
      _showSnack('Enter a placement tag first.');
      return;
    }
    _addLog(level: 'info', title: 'List Surveys', request: 'listSurveys(tag: $tag)');
    try {
      final surveys = await RapidoReach.instance.listSurveys(tag: tag);
      String? firstId;
      if (surveys.isNotEmpty && surveys.first is Map) {
        final first = Map<String, dynamic>.from(surveys.first as Map);
        final dynamic id = first['surveyIdentifier'] ??
            first['survey_id'] ??
            first['surveyId'] ??
            first['surveyID'];
        if (id is String) firstId = id;
      }
      setState(() {
        _lastSurveyId = firstId;
      });
      _addLog(
        level: 'info',
        title: 'Surveys: ${surveys.length}',
        response: firstId != null ? 'First survey id: $firstId' : 'No surveys',
      );
    } catch (e) {
      setState(() {
        _lastSurveyId = null;
      });
      _addLog(level: 'error', title: 'List Surveys failed', response: '$e');
      _showSnack('List Surveys failed: $e');
    }
  }

  Future<void> _showLastSurvey() async {
    final tag = _placementTag.trim();
    final surveyId = (_lastSurveyId ?? '').trim();
    if (tag.isEmpty) {
      _showSnack('Enter a placement tag first.');
      return;
    }
    if (surveyId.isEmpty) {
      _showSnack('List surveys first to get a survey id.');
      return;
    }
    _addLog(
      level: 'info',
      title: 'Show Survey',
      request: 'showSurvey(tag: $tag, surveyId: $surveyId)',
    );
    try {
      final params = _tryParseJsonObject(_customParamsJson);
      await RapidoReach.instance
          .showSurvey(tag: tag, surveyId: surveyId, customParams: params);
      _addLog(level: 'info', title: 'Show Survey triggered');
    } catch (e) {
      _addLog(level: 'error', title: 'Show Survey failed', response: '$e');
      _showSnack('Show Survey failed: $e');
    }
  }

  Future<void> _sendAttributes() async {
    _addLog(level: 'info', title: 'Send Attributes', request: 'sendUserAttributes(clearPrevious: false)');
    try {
      final attrs = _tryParseJsonObject(_attributesJson);
      await RapidoReach.instance.sendUserAttributes(attributes: attrs);
      _addLog(
          level: 'info',
          title: 'Attributes synced',
          response: const JsonEncoder.withIndent('  ').convert(attrs));
      _showSnack('Attributes synced');
    } catch (e) {
      _addLog(level: 'error', title: 'Send Attributes failed', response: '$e');
      _showSnack('Send Attributes failed: $e');
    }
  }

  Future<void> _toggleTheme() async {
    final next = !_usingAltTheme;
    setState(() {
      _usingAltTheme = next;
    });
    await _applyTheme(next);
    _addLog(level: 'info', title: next ? 'Theme: alt' : 'Theme: default');
  }

  Future<void> _fetchQuickQuestions() async {
    final tag = _placementTag.trim();
    if (tag.isEmpty) {
      _showSnack('Enter a placement tag first.');
      return;
    }
    _addLog(level: 'info', title: 'Fetch QQ', request: 'fetchQuickQuestions(tag: $tag)');
    try {
      final payload = await RapidoReach.instance.fetchQuickQuestions(tag: tag);
      final pretty = const JsonEncoder.withIndent('  ').convert(payload);
      _addLog(level: 'info', title: 'Quick questions received', response: pretty);
      await _showDialog('Quick questions', pretty);
    } catch (e) {
      _addLog(level: 'error', title: 'Fetch QQ failed', response: '$e');
      _showSnack('Fetch QQ failed: $e');
    }
  }

  Future<void> _answerQuickQuestion() async {
    final tag = _placementTag.trim();
    final qid = _questionId.trim();
    if (tag.isEmpty) {
      _showSnack('Enter a placement tag first.');
      return;
    }
    if (qid.isEmpty) {
      _showSnack('Enter a quick question id first.');
      return;
    }
    _addLog(level: 'info', title: 'Answer QQ', request: 'answerQuickQuestion(tag: $tag, questionId: $qid)');
    try {
      final payload = await RapidoReach.instance.answerQuickQuestion(
        tag: tag,
        questionId: qid,
        answer: _questionAnswer,
      );
      final pretty = const JsonEncoder.withIndent('  ').convert(payload);
      _addLog(level: 'info', title: 'Quick question answered', response: pretty);
      _showSnack('Quick question answered');
    } catch (e) {
      _addLog(level: 'error', title: 'Answer QQ failed', response: '$e');
      _showSnack('Answer QQ failed: $e');
    }
  }

  void _clearLogs() {
    setState(() {
      _logs.clear();
    });
  }

  void _clearRewards() {
    setState(() {
      _rewards.clear();
    });
  }

  void _simulateReward() {
    _addReward(
      amount: 25,
      placement: _placementTag,
      note: 'Manual QA bonus',
      source: 'Example app',
    );
    _addLog(level: 'event', title: 'Simulated reward: +25');
  }

  void _showSnack(String text) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
  }

  Future<void> _showDialog(String title, String body) async {
    if (!mounted) return;
    await showDialog<void>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text(title),
        content: SingleChildScrollView(child: Text(body)),
        actions: <Widget>[
          TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Close')),
        ],
      ),
    );
  }

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

    _attributesJson = const JsonEncoder.withIndent('  ').convert(<String, dynamic>{
      'qa_timestamp': DateTime.now().millisecondsSinceEpoch,
      'qa_flag': 'default',
    });

    _apiKeyController = TextEditingController(text: _apiKey);
    _userIdController = TextEditingController(text: _userId);
    _placementController = TextEditingController(text: _placementTag);
    _customParamsController = TextEditingController(text: _customParamsJson);
    _attributesController = TextEditingController(text: _attributesJson);
    _questionIdController = TextEditingController(text: _questionId);
    _questionAnswerController = TextEditingController(text: _questionAnswer);

    RapidoReach.instance.setOnRewardListener((amount) {
      final safe = amount ?? 0;
      _addReward(
        amount: safe,
        placement: _placementTag,
        note: 'SDK reward callback',
        source: 'RapidoReach SDK',
      );
      _addLog(level: 'event', title: 'onReward: +$safe');
    });

    RapidoReach.instance.setRewardCenterOpened(() {
      _addLog(level: 'event', title: 'onRewardCenterOpened');
    });

    RapidoReach.instance.setRewardCenterClosed(() {
      _addLog(level: 'event', title: 'onRewardCenterClosed');
    });

    RapidoReach.instance.setSurveyAvaiableListener((survey) {
      final normalized = (survey ?? 0) != 0;
      setState(() {
        _surveyAvailable = normalized;
      });
      _addLog(level: 'event', title: 'rapidoreachSurveyAvailable: $normalized');
    });

    RapidoReach.instance.setNetworkLogListener((payload) {
      final name = payload['name']?.toString() ?? 'SDK';
      final method = payload['method']?.toString();
      final url = payload['url']?.toString();
      final requestBody = payload['requestBody']?.toString();
      final responseBody = payload['responseBody']?.toString();
      final error = payload['error']?.toString();

      final requestParts = <String>[
        if (method != null && url != null) '$method $url',
        if (requestBody != null && requestBody.isNotEmpty) 'Body: $requestBody',
      ];

      _addLog(
        level: error != null ? 'error' : 'info',
        title: method == 'LOG' ? name : 'NET $name',
        request: requestParts.isNotEmpty ? requestParts.join('\n') : null,
        response: error ?? responseBody,
      );
    });

    RapidoReach.instance.setOnErrorListener((message) {
      if (message.trim().isEmpty) return;
      _addLog(level: 'error', title: 'onError', response: message);
    });

    RapidoReach.instance.enableNetworkLogging(enabled: true);
    RapidoReach.instance.getBaseUrl().then((value) {
      setState(() {
        _baseUrl = value;
      });
    }).catchError((_) {
      setState(() {
        _baseUrl = null;
      });
    });
  }

  @override
  void dispose() {
    _apiKeyController.dispose();
    _userIdController.dispose();
    _placementController.dispose();
    _customParamsController.dispose();
    _attributesController.dispose();
    _questionIdController.dispose();
    _questionAnswerController.dispose();
    super.dispose();
  }

  Widget _card({required String title, required List<Widget> children}) {
    return Container(
      margin: const EdgeInsets.only(bottom: 12),
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: const Color(0xFF151518),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: const Color(0xFF2A2A2E), width: 0.5),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Colors.white)),
          const SizedBox(height: 10),
          ...children,
        ],
      ),
    );
  }

  Widget _label(String text) => Padding(
        padding: const EdgeInsets.only(bottom: 6),
        child: Text(text, style: const TextStyle(fontSize: 12, color: Color(0xFF9AA0A6))),
      );

  Widget _kvRow(String label, String value) => Padding(
        padding: const EdgeInsets.only(bottom: 6),
        child: Row(
          children: <Widget>[
            Expanded(child: Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF9AA0A6)))),
            const SizedBox(width: 12),
            Expanded(
              child: Text(value, textAlign: TextAlign.right, style: const TextStyle(fontSize: 13, color: Colors.white)),
            ),
          ],
        ),
      );

  Widget _dashboard() {
    return SingleChildScrollView(
      padding: const EdgeInsets.fromLTRB(16, 16, 16, 90),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          const Text(
            'RapidoReach SDK Smoke Test (Flutter)',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white),
          ),
          const SizedBox(height: 12),
          _card(
            title: 'Setup',
            children: <Widget>[
              _label('API Key'),
              TextField(
                controller: _apiKeyController,
                onChanged: (v) => _apiKey = v,
                decoration: const InputDecoration(
                  hintText: 'YOUR_API_KEY',
                  filled: true,
                  fillColor: Color(0xFF0F0F12),
                  border: OutlineInputBorder(),
                ),
                style: const TextStyle(color: Colors.white),
              ),
              const SizedBox(height: 10),
              _label('User Id'),
              TextField(
                controller: _userIdController,
                onChanged: (v) => _userId = v,
                decoration: const InputDecoration(
                  hintText: 'YOUR_USER_ID',
                  filled: true,
                  fillColor: Color(0xFF0F0F12),
                  border: OutlineInputBorder(),
                ),
                style: const TextStyle(color: Colors.white),
              ),
              const SizedBox(height: 10),
              _label('Placement Tag'),
              TextField(
                controller: _placementController,
                onChanged: (v) => _placementTag = v,
                decoration: const InputDecoration(
                  hintText: 'default',
                  filled: true,
                  fillColor: Color(0xFF0F0F12),
                  border: OutlineInputBorder(),
                ),
                style: const TextStyle(color: Colors.white),
              ),
              const SizedBox(height: 10),
              Wrap(
                spacing: 10,
                runSpacing: 10,
                children: <Widget>[
                  ElevatedButton(
                    onPressed: _initialize,
                    child: const Text('Initialize'),
                  ),
                  OutlinedButton(
                    onPressed: _updateUserIdentifier,
                    child: const Text('Set User Id'),
                  ),
                ],
              ),
            ],
          ),
          _card(
            title: 'Status',
            children: <Widget>[
              _kvRow('Platform', Platform.isIOS ? 'ios' : 'android'),
              _kvRow('Base URL', _baseUrl ?? '—'),
              _kvRow('SDK Initialized', _sdkInitialized ? 'Yes' : 'No'),
              _kvRow(
                'Survey Available',
                _surveyAvailable == null
                    ? 'Unknown'
                    : (_surveyAvailable! ? 'Yes' : 'No'),
              ),
              _kvRow('Last Survey Id', _lastSurveyId ?? '—'),
              _kvRow('Lifetime Rewards', '$_rewardTotal coins'),
            ],
          ),
          _card(
            title: 'Offerwall',
            children: <Widget>[
              Wrap(
                spacing: 10,
                runSpacing: 10,
                children: <Widget>[
                  ElevatedButton(
                    onPressed: !_sdkInitialized || _surveyAvailable == false
                        ? null
                        : _openOfferwall,
                    child: const Text('Open Offerwall'),
                  ),
                  OutlinedButton(
                    onPressed: _toggleTheme,
                    child: const Text('Toggle Theme'),
                  ),
                ],
              ),
            ],
          ),
          _card(
            title: 'Placements & Surveys',
            children: <Widget>[
              Wrap(
                spacing: 10,
                runSpacing: 10,
                children: <Widget>[
                  OutlinedButton(
                    onPressed: _checkPlacement,
                    child: const Text('Check Placement'),
                  ),
                  OutlinedButton(
                    onPressed: _getPlacementDetails,
                    child: const Text('Details'),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              Wrap(
                spacing: 10,
                runSpacing: 10,
                children: <Widget>[
                  OutlinedButton(
                    onPressed: _listSurveys,
                    child: const Text('List Surveys'),
                  ),
                  ElevatedButton(
                    onPressed: _lastSurveyId == null ? null : _showLastSurvey,
                    child: const Text('Show Last Survey'),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              _label('Custom Params (JSON)'),
              TextField(
                controller: _customParamsController,
                maxLines: 6,
                onChanged: (v) => _customParamsJson = v,
                decoration: const InputDecoration(
                  hintText: '{}',
                  filled: true,
                  fillColor: Color(0xFF0F0F12),
                  border: OutlineInputBorder(),
                ),
                style: const TextStyle(color: Colors.white, fontFamily: 'monospace'),
              ),
            ],
          ),
          _card(
            title: 'Quick Questions',
            children: <Widget>[
              Wrap(
                spacing: 10,
                runSpacing: 10,
                children: <Widget>[
                  OutlinedButton(
                    onPressed: _fetchQuickQuestions,
                    child: const Text('Fetch QQ'),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              _label('Question Id'),
              TextField(
                controller: _questionIdController,
                onChanged: (v) => _questionId = v,
                decoration: const InputDecoration(
                  hintText: 'question_id',
                  filled: true,
                  fillColor: Color(0xFF0F0F12),
                  border: OutlineInputBorder(),
                ),
                style: const TextStyle(color: Colors.white),
              ),
              const SizedBox(height: 10),
              _label('Answer'),
              TextField(
                controller: _questionAnswerController,
                onChanged: (v) => _questionAnswer = v,
                decoration: const InputDecoration(
                  hintText: 'yes',
                  filled: true,
                  fillColor: Color(0xFF0F0F12),
                  border: OutlineInputBorder(),
                ),
                style: const TextStyle(color: Colors.white),
              ),
              const SizedBox(height: 10),
              ElevatedButton(
                onPressed: _answerQuickQuestion,
                child: const Text('Answer QQ'),
              ),
            ],
          ),
          _card(
            title: 'User Attributes',
            children: <Widget>[
              _label('Attributes (JSON)'),
              TextField(
                controller: _attributesController,
                maxLines: 6,
                onChanged: (v) => _attributesJson = v,
                decoration: const InputDecoration(
                  hintText: '{"key":"value"}',
                  filled: true,
                  fillColor: Color(0xFF0F0F12),
                  border: OutlineInputBorder(),
                ),
                style: const TextStyle(color: Colors.white, fontFamily: 'monospace'),
              ),
              const SizedBox(height: 10),
              ElevatedButton(
                onPressed: _sendAttributes,
                child: const Text('Send Attributes'),
              ),
            ],
          ),
          _card(
            title: 'Notes',
            children: const <Widget>[
              Text(
                'This app mirrors the React Native example: initialize the SDK, watch survey availability events, open the offerwall, test placements/surveys/quick questions, and record rewards/logs.',
                style: TextStyle(fontSize: 14, color: Color(0xFFD6D6D6), height: 1.4),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _rewardsTab() {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              const Text('Rewards', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white)),
              const SizedBox(height: 6),
              Text('$_rewardTotal coins total', style: const TextStyle(color: Color(0xFFD6D6D6))),
              const SizedBox(height: 10),
              Wrap(
                spacing: 10,
                children: <Widget>[
                  OutlinedButton(onPressed: _simulateReward, child: const Text('Simulate +25')),
                  OutlinedButton(onPressed: _clearRewards, child: const Text('Clear')),
                ],
              ),
            ],
          ),
        ),
        Expanded(
          child: ListView.builder(
            padding: const EdgeInsets.fromLTRB(16, 0, 16, 90),
            itemCount: _rewards.isEmpty ? 1 : _rewards.length,
            itemBuilder: (context, index) {
              if (_rewards.isEmpty) {
                return const Text('No rewards yet. Complete a survey.',
                    style: TextStyle(color: Color(0xFF9AA0A6)));
              }
              final item = _rewards[index];
              return Container(
                margin: const EdgeInsets.only(bottom: 10),
                padding: const EdgeInsets.all(14),
                decoration: BoxDecoration(
                  color: const Color(0xFF151518),
                  borderRadius: BorderRadius.circular(12),
                  border: Border.all(color: const Color(0xFF2A2A2E), width: 0.5),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text('+${item.amount} coins · ${item.placement}',
                        style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700)),
                    const SizedBox(height: 4),
                    Text('${item.note} · ${_formatTimestamp(item.timestampMs)}',
                        style: const TextStyle(color: Color(0xFF9AA0A6))),
                  ],
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _logsTab() {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              const Text('Logs', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white)),
              OutlinedButton(onPressed: _clearLogs, child: const Text('Clear')),
            ],
          ),
        ),
        Expanded(
          child: ListView.builder(
            padding: const EdgeInsets.fromLTRB(16, 0, 16, 90),
            itemCount: _logs.isEmpty ? 1 : _logs.length,
            itemBuilder: (context, index) {
              if (_logs.isEmpty) {
                return const Text('No logs yet. Trigger an action.',
                    style: TextStyle(color: Color(0xFF9AA0A6)));
              }
              final item = _logs[index];
              return Container(
                margin: const EdgeInsets.only(bottom: 10),
                padding: const EdgeInsets.all(14),
                decoration: BoxDecoration(
                  color: const Color(0xFF151518),
                  borderRadius: BorderRadius.circular(12),
                  border: Border.all(color: const Color(0xFF2A2A2E), width: 0.5),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text('[${item.level}] ${item.title}',
                        style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700)),
                    const SizedBox(height: 4),
                    Text(_formatTimestamp(item.timestampMs),
                        style: const TextStyle(color: Color(0xFF9AA0A6))),
                    if (item.request != null && item.request!.isNotEmpty) ...<Widget>[
                      const SizedBox(height: 6),
                      Text('Request: ${item.request!}',
                          style: const TextStyle(color: Color(0xFFB7BBC0), fontFamily: 'monospace', fontSize: 12)),
                    ],
                    if (item.response != null && item.response!.isNotEmpty) ...<Widget>[
                      const SizedBox(height: 6),
                      Text('Response: ${item.response!}',
                          style: const TextStyle(color: Color(0xFFB7BBC0), fontFamily: 'monospace', fontSize: 12)),
                    ],
                  ],
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final Widget body;
    if (_tab == DemoTab.dashboard) {
      body = _dashboard();
    } else if (_tab == DemoTab.rewards) {
      body = _rewardsTab();
    } else {
      body = _logsTab();
    }

    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: const Color(0xFF0B0B0C),
        snackBarTheme: const SnackBarThemeData(backgroundColor: Color(0xFF23232A)),
      ),
      home: Scaffold(
        body: SafeArea(
          child: body,
        ),
        bottomNavigationBar: BottomNavigationBar(
          currentIndex: _tab.index,
          onTap: (idx) => setState(() => _tab = DemoTab.values[idx]),
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(icon: Icon(Icons.dashboard), label: 'Dashboard'),
            BottomNavigationBarItem(icon: Icon(Icons.card_giftcard), label: 'Rewards'),
            BottomNavigationBarItem(icon: Icon(Icons.list), label: 'Logs'),
          ],
        ),
      ),
    );
  }
}
5
likes
120
points
109
downloads

Publisher

verified publisherrapidoreach.com

Weekly Downloads

Monetize your users through rewarded surveys!

Homepage

Documentation

API reference

License

unknown (license)

Dependencies

flutter

More

Packages that depend on rapidoreach

Packages that implement rapidoreach