cf_waiting_room
Disclaimer: This package is an unofficial community library and is not affiliated with, endorsed by, or supported by Cloudflare, Inc. Cloudflare® is a registered trademark of Cloudflare, Inc.
An unofficial Flutter widget that gates your app behind a Cloudflare Waiting Room.
How it works — the user journey
Imagine your app is a ticket-sale event backed by a Cloudflare Waiting Room.
User opens app
│
▼
┌─────────────────────────────┐
│ Phase 1 – Live CF page │ Full-screen WebView.
│ (WebView full-screen) │ User sees the real CF queue page immediately.
└────────────┬────────────────┘
│ CF confirms queue active
▼
┌─────────────────────────────┐
│ Phase 2 – Native overlay │ Your brand UI replaces the raw CF page.
│ (overlay + 1×1 WebView) │ The tiny invisible WebView keeps CF's JS
└────────────┬────────────────┘ running so the session cookie stays valid.
│ CF redirects → pass page (title contains passKeyWord)
▼
┌─────────────────────────────┐
│ Phase 3 – Silent monitor │ onQueueDone() fires — your app is shown.
│ (invisible 1×1 WebView) │ A session timer continues ticking in the
└────────────┬────────────────┘ background to detect re-queue situations.
│ sessionTimeout fires
▼
WebView reloads silently
┌── queue active? ──▶ onNeedReQueue() → back to Phase 2
└── still clear? ──▶ onSessionTimeout() → timer restarts
Installation
dependencies:
cf_waiting_room: ^0.3.2
Quick start
Use CFWaitingRoomGate — the drop-in root widget that manages the Stack,
remount key, and queue-done state for you:
final _config = WaitingRoomConfig(
isEnable: true,
queueUrl: 'https://your-site.com/',
queueKeyWord: ['waiting', 'queue'],
passKeyWord: ['myapp'], // substring of the real app page title
sessionTimeoutMinutes: 25, // post-pass monitoring interval
isEnterprise: false, // true if your CF zone is Enterprise
);
@override
Widget build(BuildContext context) {
return CFWaitingRoomGate(
config: _config,
// appBuilder receives onReQueue — pass it to forceReQueue.onConfirm
appBuilder: (context, onReQueue) => YourAppContent(
onPurchaseComplete: () => CFWaitingRoomOverlayWidget.forceReQueue(
context,
config: _config,
onConfirm: onReQueue, // ← gate resets to Phase 1 automatically
),
),
onSessionTimeout: () => _showSessionExpiredBanner(),
);
}
Need full control over the Stack layout? Use CFWaitingRoomOverlayWidget
directly (must be a child of a Stack, uses Positioned internally):
Stack(
children: [
if (_queueDone) YourAppContent(),
CFWaitingRoomOverlayWidget(
config: _config,
onQueueDone: () => setState(() => _queueDone = true),
onNeedReQueue: () => setState(() { _queueDone = false; generation++; }),
),
],
)
Session timeout & CF plan tier
sessionTimeoutMinutes starts counting after the queue is passed (Phase 3).
When it fires the widget revokes the CF session and reloads silently to re-check
whether the CF queue is active again.
WaitingRoomConfig(
sessionTimeoutMinutes: 25, // set to your CF waiting room session duration
isEnterprise: false, // default — use true for Enterprise zones
)
isEnterprise flag
Cloudflare's session revocation via the Cf-Waiting-Room-Command: revoke HTTP
header is an Enterprise-only feature.
| Plan | Session reset method |
|---|---|
| Free / Pro / Business | Cookie jar + cache cleared locally in the WebView |
| Enterprise | Cf-Waiting-Room-Command: revoke header — CF frees the slot server-side immediately |
Non-enterprise grace period: CF automatically renews the
__cfwaitingroom_* cookie expiry on every WebView request. The widget
automatically adds 60 seconds to sessionTimeoutMinutes when
isEnterprise is false, so the timer fires after the cookie has genuinely
expired. Set sessionTimeoutMinutes equal to your CF session duration — the
widget handles the extra 60 s internally.
// CF waiting room session = 20 min, non-enterprise
// Widget fires after 21 min (20 min + 60 s grace)
WaitingRoomConfig(
sessionTimeoutMinutes: 20,
isEnterprise: false, // default
)
// CF waiting room session = 20 min, enterprise
// Widget fires after exactly 20 min + revoke header sent
WaitingRoomConfig(
sessionTimeoutMinutes: 20,
isEnterprise: true,
)
Mock mode — test the full flow without a live CF endpoint
CFWaitingRoomGate(
config: _config,
mockConfig: MockConfig(
isEnable: true,
waitDuration: Duration(seconds: 10), // auto-pass after 10 s
),
appBuilder: (context, onReQueue) => YourAppContent(onReQueue: onReQueue),
onSessionTimeout: () => _showBanner('Session expired'),
)
Mock timeline (with sessionTimeoutMinutes: 1, waitDuration: 10s):
| t | Event |
|---|---|
| 0 s | Mock queue HTML loads → Phase 1 → Phase 2 overlay |
| 10 s | Auto-pass → onQueueDone() fires, your app appears (Phase 3) |
| ~70 s | Dialog: "Yes — still in queue" → onNeedReQueue() + reset to Phase 1 |
"No — queue cleared" → onSessionTimeout() + timer restarts |
Force re-queue (e.g. after a successful purchase)
// Inside appBuilder — onReQueue is provided by CFWaitingRoomGate
appBuilder: (context, onReQueue) => ElevatedButton(
onPressed: () => CFWaitingRoomOverlayWidget.forceReQueue(
context,
config: _config,
onConfirm: onReQueue, // gate resets to Phase 1 automatically
),
child: const Text('Re-queue'),
),
Customise the default overlay
No full custom builder needed — pass widgets and styles directly:
CFWaitingRoomGate(
config: _config,
appBuilder: (context, onReQueue) => MyApp(onReQueue: onReQueue),
overlayIcon: Image.asset('assets/logo.png', height: 64),
loadingIcon: Image.asset('assets/spinner.gif', width: 56, height: 56),
overlayBackgroundColor: const Color(0xFF0D1B2A),
titleStyle: const TextStyle(color: Colors.amber, fontSize: 22),
refreshMessageStyle: const TextStyle(color: Colors.white60, fontSize: 14),
)
Text labels driven from WaitingRoomConfig (Remote Config-friendly):
| Config field | Description |
|---|---|
waitingTitle |
Overrides CF page <h1> — always shown |
waitingRefreshMessage |
Body copy below the ETA |
lastUpdatedPrefix |
Prefix before the last-updated timestamp |
Locale — Accept-Language header
Resolution order:
CFWaitingRoomGate.locale— widget-levelLocaleoverrideWaitingRoomConfig.locale— Remote Config BCP-47 string, e.g."zh-HK"- Device system locale (
PlatformDispatcher.instance.locale)
CFWaitingRoomGate(
config: _config,
locale: const Locale('zh', 'HK'),
appBuilder: (context, onReQueue) => MyApp(onReQueue: onReQueue),
)
Custom UI builders
Phase 2 waiting overlay
CFWaitingRoomGate(
config: _config,
appBuilder: (context, onReQueue) => MyApp(onReQueue: onReQueue),
waitingOverlayBuilder: (context, info) => MyWaitingScreen(
title: info.title,
eta: info.eta,
lastUpdated: info.lastUpdated,
),
)
Force re-queue page
// Pass pageBuilder to CFWaitingRoomOverlayWidget.forceReQueue
CFWaitingRoomOverlayWidget.forceReQueue(
context,
config: _config,
onConfirm: onReQueue,
pageBuilder: (context, onConfirm) => MyReQueuePage(onConfirm: onConfirm),
);
Advanced — autoReQueue control
By default, when the session timer fires the gate automatically revokes the CF
cookie and transitions back to Phase 2 if the queue is active again. Set
autoReQueue: false in WaitingRoomConfig to take manual control:
WaitingRoomConfig(
sessionTimeoutMinutes: 25,
autoReQueue: false, // widget revokes cookie only
)
// Then use overlayKey to trigger the check yourself:
final _overlayKey = GlobalKey<CFWaitingRoomOverlayWidgetState>();
CFWaitingRoomGate(
config: _config,
overlayKey: _overlayKey,
onSessionTimeout: () async {
await _myAppSignOut(); // your async work first
_overlayKey.currentState?.checkQueueStatus(); // then trigger re-check
},
appBuilder: (context, onReQueue) => MyApp(onReQueue: onReQueue),
)
Firebase Remote Config integration
{
"isEnable": true,
"queueUrl": "https://your-site.com/",
"queueKeyWord": ["waiting", "queue"],
"passKeyWord": ["myapp"],
"etaId": "waitTime",
"lastUpdatedId": "last-updated",
"sessionTimeoutMinutes": 25,
"isEnterprise": false,
"clearCookieOnStart": true,
"locale": "zh-HK",
"waitingTitle": "您正在排隊中,感謝耐心等候。",
"waitingRefreshMessage": "本頁將自動更新,請勿關閉應用程式。",
"lastUpdatedPrefix": "最後更新:",
"reQueueDialogMessage": "恭喜您搶購成功!若想再次購買,請重新排隊。",
"reQueueDialogBtnText": "確定並重新排隊"
}
final raw = remoteConfig.getString('waitingRoomConfig');
final config = raw.isNotEmpty
? WaitingRoomConfig.fromJson(jsonDecode(raw))
: WaitingRoomConfig(isEnable: false);
訪特權 mode (skip-queue pass)
Set clearCookieOnStart: false to preserve existing CF cookies so a returning
user skips the queue if their session is still valid.
Platform support
| Platform | Support |
|---|---|
| Android | ✅ |
| iOS | ✅ |
| Web | ❌ (webview_flutter not supported on Web) |
Phase-aware error handling
Network errors inside the WebView are handled differently per phase so a transient connectivity blip never silently lets the user skip the queue:
| Phase | Main-frame error behaviour |
|---|---|
| Phase 1 (WebView full-screen) | onQueueDone() is called — graceful fallback, app is shown. |
| Phase 2 (native overlay + 1×1 WebView) | Error is ignored; CF's own auto-refresh JS will retry automatically. |
| Phase 3 (invisible WebView, post-pass monitoring) | Session timer restarts; the error is never treated as a queue pass. |
Libraries
- cf_waiting_room
- Flutter widget for Cloudflare Waiting Room integration.