cf_waiting_room 0.3.1 copy "cf_waiting_room: ^0.3.1" to clipboard
cf_waiting_room: ^0.3.1 copied to clipboard

Unofficial Flutter widget for integrating with Cloudflare Waiting Room — WebView-based queue gate with native overlay, session timeout, force re-queue flow, and custom UI builders.

example/lib/main.dart

import 'package:cf_waiting_room/cf_waiting_room.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  await dotenv.load(fileName: '.env');
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'cf_waiting_room demo',
      home: _GatePage(),
    );
  }
}

// ── Gate page ─────────────────────────────────────────────────────────────

class _GatePage extends StatefulWidget {
  const _GatePage();

  @override
  State<_GatePage> createState() => _GatePageState();
}

class _GatePageState extends State<_GatePage> {
  bool _queueDone = false;
  int _widgetGeneration = 0;

  // Toggleable config flags
  bool _isEnterprise = false;

  WaitingRoomConfig get _config => WaitingRoomConfig(
        isEnable: true,
        queueUrl: dotenv.env['QUEUE_URL'] ?? 'https://your-site.com/',
        queueKeyWord: ['waiting', 'queue', '等候'],
        passKeyWord: ['PayKool'],
        etaId: 'waitTime',
        lastUpdatedId: 'last-updated',
        sessionTimeoutMinutes: 1,
        clearCookieOnStart: true,
        isEnterprise: _isEnterprise,
        locale: 'zh-HK',
        waitingTitle: '您正在排隊中…',
        waitingRefreshMessage: '目前使用人數較多,請稍作等候。\n系統會盡快為您處理,感謝您的耐心等候。\n\n'
            '此頁面將自動重新整理,請勿關閉應用程式。',
        lastUpdatedPrefix: '最後更新:',
        reQueueDialogMessage: '恭喜您搶購成功!為確保公平,您的本次優先通行證已使用完畢。\n若想再次購買,請重新排隊。',
        reQueueDialogBtnText: '確定並重新排隊',
      );

  void _onQueueDone() => setState(() => _queueDone = true);

  void _onNeedReQueue() => setState(() {
        _queueDone = false;
        _widgetGeneration++;
      });

  void _toggleEnterprise(bool value) => setState(() {
        _isEnterprise = value;
        // Remount widget so the new isEnterprise value takes effect immediately.
        _widgetGeneration++;
      });

  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      child: Scaffold(
        backgroundColor: Colors.black,
        body: Stack(
          children: [
            if (_queueDone)
              _AppPage(
                onNeedReQueue: _onNeedReQueue,
                isEnterprise: _isEnterprise,
                onToggleEnterprise: _toggleEnterprise,
                config: _config,
              ),
            CFWaitingRoomOverlayWidget(
              key: ValueKey(_widgetGeneration),
              config: _config,
              onQueueDone: _onQueueDone,
              onSessionTimeout: () {
                if (!mounted) return;
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text('⏱ Session expired — checking queue…'),
                    duration: Duration(seconds: 2),
                  ),
                );
              },
              onNeedReQueue: _onNeedReQueue,
              mockConfig: MockConfig(
                isEnable: false,
                waitDuration: const Duration(seconds: 10),
              ),
              overlayIcon: const Icon(
                Icons.stadium_outlined,
                color: Colors.white70,
                size: 48,
              ),
              overlayBackgroundColor: const Color(0xFF1A2C45),
              titleStyle: const TextStyle(
                color: Colors.white,
                fontSize: 20,
                fontWeight: FontWeight.bold,
                height: 1.5,
              ),
              refreshMessageStyle: const TextStyle(
                color: Colors.white70,
                fontSize: 13,
                height: 1.5,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ── App page — scenario menu ──────────────────────────────────────────────

class _AppPage extends StatelessWidget {
  const _AppPage({
    required this.onNeedReQueue,
    required this.isEnterprise,
    required this.onToggleEnterprise,
    required this.config,
  });

  final VoidCallback onNeedReQueue;
  final bool isEnterprise;
  final ValueChanged<bool> onToggleEnterprise;
  final WaitingRoomConfig config;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0D1B2A),
      appBar: AppBar(
        backgroundColor: const Color(0xFF1A2C45),
        title:
            const Text('✅ Queue Passed', style: TextStyle(color: Colors.white)),
        automaticallyImplyLeading: false,
      ),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // ── Status card ──────────────────────────────────────────
              Container(
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: Colors.green.withValues(alpha: 0.15),
                  borderRadius: BorderRadius.circular(12),
                  border: Border.all(
                      color: Colors.greenAccent.withValues(alpha: 0.4)),
                ),
                child: const Row(
                  children: [
                    Icon(Icons.check_circle,
                        color: Colors.greenAccent, size: 28),
                    SizedBox(width: 12),
                    Expanded(
                      child: Text(
                        'You are in the app!\nCF queue has been passed.',
                        style: TextStyle(color: Colors.white, height: 1.5),
                      ),
                    ),
                  ],
                ),
              ),
              const SizedBox(height: 24),

              // ── Config toggles ───────────────────────────────────────
              const Text(
                'CONFIG',
                style: TextStyle(
                  color: Colors.white54,
                  fontSize: 11,
                  letterSpacing: 1.5,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 8),
              _ConfigToggle(
                icon: Icons.business,
                label: 'isEnterprise',
                description: isEnterprise
                    ? 'Revoke via Cf-Waiting-Room-Command header'
                    : 'Clear cookie jar + cache locally',
                value: isEnterprise,
                onChanged: onToggleEnterprise,
                activeColor: Colors.amberAccent,
              ),
              const SizedBox(height: 24),

              // ── Scenario menu ────────────────────────────────────────
              const Text(
                'TEST SCENARIOS',
                style: TextStyle(
                  color: Colors.white54,
                  fontSize: 11,
                  letterSpacing: 1.5,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 12),

              _ScenarioTile(
                icon: Icons.shopping_cart_checkout,
                title: 'Force Re-Queue',
                subtitle:
                    'Built-in confirmation dialog → releases slot → Phase 1.',
                color: Colors.orangeAccent,
                onTap: () => CFWaitingRoomOverlayWidget.forceReQueue(
                  context,
                  config: config,
                  onConfirm: onNeedReQueue,
                ),
              ),
              const SizedBox(height: 12),

              _ScenarioTile(
                icon: Icons.shopping_bag_outlined,
                title: 'Force Re-Queue (Custom Page)',
                subtitle: 'Same flow with a custom confirmation screen.',
                color: Colors.purpleAccent,
                onTap: () => CFWaitingRoomOverlayWidget.forceReQueue(
                  context,
                  config: config,
                  onConfirm: onNeedReQueue,
                  pageBuilder: (ctx, onConfirm) =>
                      _CustomReQueuePage(onConfirm: onConfirm),
                ),
              ),
              const SizedBox(height: 12),

              _ScenarioTile(
                icon: Icons.refresh,
                title: 'Instant Re-Queue (no dialog)',
                subtitle: 'Directly fires onNeedReQueue — no confirmation.',
                color: Colors.blueAccent,
                onTap: onNeedReQueue,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ── Config toggle row ─────────────────────────────────────────────────────

class _ConfigToggle extends StatelessWidget {
  const _ConfigToggle({
    required this.icon,
    required this.label,
    required this.description,
    required this.value,
    required this.onChanged,
    required this.activeColor,
  });

  final IconData icon;
  final String label;
  final String description;
  final bool value;
  final ValueChanged<bool> onChanged;
  final Color activeColor;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      decoration: BoxDecoration(
        color: value
            ? activeColor.withValues(alpha: 0.1)
            : Colors.white.withValues(alpha: 0.05),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(
          color: value
              ? activeColor.withValues(alpha: 0.4)
              : Colors.white.withValues(alpha: 0.1),
        ),
      ),
      child: Row(
        children: [
          Icon(icon, color: value ? activeColor : Colors.white38, size: 22),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(label,
                    style: TextStyle(
                        color: value ? activeColor : Colors.white70,
                        fontWeight: FontWeight.bold,
                        fontSize: 13)),
                Text(description,
                    style: const TextStyle(
                        color: Colors.white38, fontSize: 11, height: 1.4)),
              ],
            ),
          ),
          Switch(
            value: value,
            onChanged: onChanged,
            activeThumbColor: activeColor,
            activeTrackColor: activeColor.withValues(alpha: 0.4),
          ),
        ],
      ),
    );
  }
}

// ── Scenario tile ─────────────────────────────────────────────────────────

class _ScenarioTile extends StatelessWidget {
  const _ScenarioTile({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.color,
    required this.onTap,
  });

  final IconData icon;
  final String title;
  final String subtitle;
  final Color color;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Container(
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: color.withValues(alpha: 0.1),
            borderRadius: BorderRadius.circular(12),
            border: Border.all(color: color.withValues(alpha: 0.35)),
          ),
          child: Row(
            children: [
              Icon(icon, color: color, size: 28),
              const SizedBox(width: 14),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(title,
                        style: TextStyle(
                            color: color,
                            fontWeight: FontWeight.bold,
                            fontSize: 14)),
                    const SizedBox(height: 4),
                    Text(subtitle,
                        style: const TextStyle(
                            color: Colors.white60, fontSize: 12, height: 1.4)),
                  ],
                ),
              ),
              Icon(Icons.chevron_right, color: color.withValues(alpha: 0.6)),
            ],
          ),
        ),
      ),
    );
  }
}

// ── Custom Phase 2 overlay (optional example) ─────────────────────────────

class _CustomWaitingOverlay extends StatelessWidget {
  const _CustomWaitingOverlay({required this.info});

  final QueueWaitingInfo info;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: const Color(0xFF0D1B2A),
      child: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const CircularProgressIndicator(color: Colors.amber),
            const SizedBox(height: 24),
            Text(
              info.title ?? 'You are in the queue.',
              style: const TextStyle(color: Colors.white, fontSize: 20),
              textAlign: TextAlign.center,
            ),
            if (info.eta != null) ...[
              const SizedBox(height: 12),
              Text('ETA: ${info.eta}',
                  style: const TextStyle(color: Colors.amber)),
            ],
            if (info.lastUpdated != null) ...[
              const SizedBox(height: 8),
              Text('Updated: ${info.lastUpdated}',
                  style: const TextStyle(color: Colors.white54, fontSize: 12)),
            ],
          ],
        ),
      ),
    );
  }
}

// ── Custom forceReQueue page (optional example) ───────────────────────────

class _CustomReQueuePage extends StatelessWidget {
  const _CustomReQueuePage({required this.onConfirm});

  final VoidCallback onConfirm;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0D1B2A),
      body: SafeArea(
        child: Center(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 32),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.check_circle,
                    color: Colors.greenAccent, size: 72),
                const SizedBox(height: 24),
                const Text(
                  'Purchase successful!\nPlease re-queue for another attempt.',
                  textAlign: TextAlign.center,
                  style:
                      TextStyle(color: Colors.white, fontSize: 18, height: 1.5),
                ),
                const SizedBox(height: 40),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: onConfirm,
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.greenAccent,
                      foregroundColor: Colors.black,
                      padding: const EdgeInsets.symmetric(vertical: 16),
                      shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(12)),
                    ),
                    child: const Text('Re-join queue',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
1
likes
150
points
257
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Unofficial Flutter widget for integrating with Cloudflare Waiting Room — WebView-based queue gate with native overlay, session timeout, force re-queue flow, and custom UI builders.

Repository (GitHub)
View/report issues

Topics

#webview #cloudflare #waiting-room #queue

License

MIT (license)

Dependencies

flutter, json_annotation, shared_preferences, webview_flutter

More

Packages that depend on cf_waiting_room