Skip to content

Commit c6c50bf

Browse files
feat: add timeouts to loading overlay [#142] (#144)
* feat: add timeouts to loading overlay [#142] * fix: remove unnecessary logging import [#142] * fix: add tests [#142] * fix: ensure overlay count can't go below zero [#142] * fix: pr changes * fix: add test report to automation --------- Co-authored-by: Simon Lightfoot <[email protected]>
1 parent 4ec8f1c commit c6c50bf

File tree

9 files changed

+341
-34
lines changed

9 files changed

+341
-34
lines changed

.github/workflows/main.yaml

+13-1
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,17 @@ jobs:
5252
echo $TEST_ENV_BASE64 | base64 --decode > packages/clerk_auth/.env.test
5353
5454
- name: Run tests in clerk_auth
55-
run: dart test
55+
run: dart test --file-reporter=json:../../reports/clerk_auth.json
5656
working-directory: packages/clerk_auth
57+
58+
- name: Run tests in clerk_flutter
59+
run: flutter test --file-reporter=json:../../reports/clerk_flutter.json
60+
working-directory: packages/clerk_flutter
61+
62+
- name: Test Report
63+
uses: dorny/test-reporter@v1
64+
if: success() || failure() # run this step even if previous step failed
65+
with:
66+
name: 'Test Report' # Name of the check run which will be created
67+
path: reports/*.json # Path to test results
68+
reporter: flutter-json # Format of test results

packages/clerk_flutter/lib/src/clerk_auth_state.dart

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:clerk_auth/clerk_auth.dart' as clerk;
44
import 'package:clerk_flutter/clerk_flutter.dart';
55
import 'package:clerk_flutter/src/utils/localization_extensions.dart';
66
import 'package:clerk_flutter/src/widgets/ui/clerk_loading_overlay.dart';
7+
import 'package:clerk_flutter/src/widgets/ui/clerk_overlay_host.dart';
78
import 'package:collection/collection.dart';
89
import 'package:flutter/material.dart';
910
import 'package:path_provider/path_provider.dart';
@@ -177,13 +178,14 @@ class ClerkAuthState extends clerk.Auth with ChangeNotifier {
177178
ClerkErrorCallback? onError,
178179
}) async {
179180
T? result;
180-
_loadingOverlay.show(context);
181+
final overlay = ClerkOverlay.of(context);
182+
_loadingOverlay.insertInto(overlay);
181183
try {
182184
result = await fn();
183185
} on clerk.AuthError catch (error) {
184186
_onError(error, onError);
185187
} finally {
186-
_loadingOverlay.hide();
188+
_loadingOverlay.removeFrom(overlay);
187189
}
188190
return result;
189191
}

packages/clerk_flutter/lib/src/utils/clerk_auth_config.dart

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:clerk_auth/clerk_auth.dart' as clerk;
22
import 'package:clerk_flutter/clerk_flutter.dart';
33
import 'package:clerk_flutter/src/generated/clerk_sdk_localizations_en.dart';
4+
import 'package:clerk_flutter/src/widgets/ui/common.dart'
5+
show defaultLoadingWidget;
46
import 'package:flutter/material.dart';
57

68
/// A map of [Locale] strings to [ClerkSdkLocalizations] instances
@@ -23,7 +25,7 @@ class ClerkAuthConfig extends clerk.AuthConfig {
2325
super.telemetryPeriod,
2426
super.clientRefreshPeriod,
2527
ClerkSdkLocalizationsCollection? localizations,
26-
this.loading,
28+
this.loading = defaultLoadingWidget,
2729
}) : _localizations = localizations;
2830

2931
static ClerkSdkLocalizationsCollection get _defaultLocalizations =>
@@ -34,7 +36,8 @@ class ClerkAuthConfig extends clerk.AuthConfig {
3436
_localizations ?? _defaultLocalizations;
3537
final ClerkSdkLocalizationsCollection? _localizations;
3638

37-
/// The [Widget] to display while loading data
39+
/// The [Widget] to display while loading data, override with null
40+
/// to disable the loading overlay or use your own widget.
3841
final Widget? loading;
3942

4043
@override

packages/clerk_flutter/lib/src/widgets/control/clerk_auth.dart

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:async';
33
import 'package:clerk_auth/clerk_auth.dart' as clerk;
44
import 'package:clerk_flutter/clerk_flutter.dart';
55
import 'package:clerk_flutter/src/utils/clerk_telemetry.dart';
6+
import 'package:clerk_flutter/src/widgets/ui/clerk_overlay_host.dart';
67
import 'package:clerk_flutter/src/widgets/ui/common.dart';
78
import 'package:flutter/material.dart';
89

@@ -132,7 +133,9 @@ class _ClerkAuthState extends State<ClerkAuth> with ClerkTelemetryStateMixin {
132133
builder: (BuildContext context, Widget? child) {
133134
return _ClerkAuthData(
134135
authState: authState,
135-
child: widget.child,
136+
child: ClerkOverlayHost(
137+
child: widget.child,
138+
),
136139
);
137140
},
138141
);
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,79 @@
1+
import 'dart:async';
2+
13
import 'package:clerk_flutter/src/utils/clerk_auth_config.dart';
2-
import 'package:clerk_flutter/src/widgets/ui/common.dart';
4+
import 'package:clerk_flutter/src/widgets/ui/clerk_overlay_host.dart';
35
import 'package:flutter/widgets.dart';
46

57
/// Clerk Loading Overlay
68
class ClerkLoadingOverlay {
79
/// Constructs a [ClerkLoadingOverlay]
8-
ClerkLoadingOverlay(ClerkAuthConfig config)
9-
: _overlayEntry = OverlayEntry(
10-
builder: (context) => config.loading ?? defaultLoadingWidget,
11-
);
10+
ClerkLoadingOverlay(ClerkAuthConfig config) : _loadingWidget = config.loading;
11+
12+
/// The delay between an [insertInto] call and the loading overlay
13+
/// being displayed
14+
static const startupDuration = Duration(milliseconds: 300);
15+
16+
/// The minimum amount of time the loading overlay should remain
17+
/// on screen for
18+
static const minimumOnScreenDuration = Duration(milliseconds: 800);
19+
20+
/// The number of [ClerkLoadingOverlay] requests that are currently
21+
/// pending
22+
int count = 0;
23+
24+
Timer? _displayTimer;
25+
Timer? _hideTimer;
26+
DateTime _hideAfter = DateTime(0);
1227

13-
final OverlayEntry _overlayEntry;
28+
final Widget? _loadingWidget;
1429

1530
/// Shows the loading overlay
16-
void show(BuildContext context) {
17-
final overlay = Overlay.of(context);
18-
if (overlay.context.mounted && _overlayEntry.mounted == false) {
19-
overlay.insert(_overlayEntry);
31+
void insertInto(ClerkOverlay overlay) {
32+
// no overlay widget was supplied so we dont try and display it
33+
if (_loadingWidget == null) {
34+
return;
35+
}
36+
// make the display of loading indicator reentrant
37+
if (++count == 1) {
38+
_hideTimer?.cancel();
39+
_hideTimer = null;
40+
_displayTimer ??= Timer(
41+
startupDuration,
42+
() {
43+
if (!overlay.isDisplaying(_loadingWidget!)) {
44+
_hideAfter = DateTime.timestamp().add(minimumOnScreenDuration);
45+
overlay.insert(_loadingWidget!);
46+
}
47+
},
48+
);
2049
}
2150
}
2251

2352
/// Hides the loading overlay
24-
void hide() {
25-
_overlayEntry.remove();
53+
void removeFrom(ClerkOverlay overlay) {
54+
// no overlay widget was supplied so we dont try and remove it
55+
if (_loadingWidget == null) {
56+
return;
57+
}
58+
// make the display of loading indicator reentrant
59+
if (count > 0 && --count == 0) {
60+
_displayTimer?.cancel();
61+
_displayTimer = null;
62+
63+
if (_hideTimer == null && overlay.isDisplaying(_loadingWidget!)) {
64+
final now = DateTime.timestamp();
65+
if (_hideAfter.isBefore(now)) {
66+
overlay.remove(_loadingWidget!);
67+
} else {
68+
_hideTimer = Timer(
69+
_hideAfter.difference(now),
70+
() {
71+
_hideTimer = null;
72+
overlay.remove(_loadingWidget!);
73+
},
74+
);
75+
}
76+
}
77+
}
2678
}
2779
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
/// Abstract class to provide access to displaying an overlay
4+
abstract class ClerkOverlay<T extends StatefulWidget> extends State<T> {
5+
/// Insert an [Widget]
6+
void insert(Widget overlay);
7+
8+
/// Remove an [Widget]
9+
void remove(Widget overlay);
10+
11+
/// Is the [Widget] on display?
12+
bool isDisplaying(Widget overlay);
13+
14+
/// Find the overlay host in widget tree
15+
static ClerkOverlay of(BuildContext context) {
16+
return context.findAncestorStateOfType<ClerkOverlay>()!;
17+
}
18+
}
19+
20+
/// Displays widgets overlaying content of [child]
21+
class ClerkOverlayHost extends StatefulWidget {
22+
/// Constructs a [ClerkOverlayHost]
23+
const ClerkOverlayHost({
24+
super.key,
25+
required this.child,
26+
});
27+
28+
/// Child widget to wrap
29+
final Widget child;
30+
31+
@override
32+
State<ClerkOverlayHost> createState() => _ClerkOverlayHostState();
33+
}
34+
35+
class _ClerkOverlayHostState extends State<ClerkOverlayHost>
36+
implements ClerkOverlay<ClerkOverlayHost> {
37+
final _overlays = <Widget>[];
38+
39+
@override
40+
bool isDisplaying(Widget overlay) => _overlays.contains(overlay);
41+
42+
@override
43+
void insert(Widget overlay) {
44+
setState(() => _overlays.add(overlay));
45+
}
46+
47+
@override
48+
void remove(Widget overlay) {
49+
setState(() => _overlays.remove(overlay));
50+
}
51+
52+
@override
53+
Widget build(BuildContext context) {
54+
return Stack(
55+
fit: StackFit.passthrough,
56+
textDirection: TextDirection.ltr,
57+
children: [
58+
IgnorePointer(
59+
ignoring: _overlays.isNotEmpty,
60+
child: widget.child,
61+
),
62+
..._overlays,
63+
],
64+
);
65+
}
66+
}

packages/clerk_flutter/lib/src/widgets/ui/common.dart

+1-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ final defaultOrgLogo = SvgPicture.asset(
1515
package: 'clerk_flutter',
1616
);
1717

18-
const defaultLoadingWidget = SizedBox(
19-
width: double.infinity,
20-
height: double.infinity,
18+
const defaultLoadingWidget = Positioned.fill(
2119
child: ColoredBox(
2220
color: Colors.black26,
2321
child: Center(child: CircularProgressIndicator()),

packages/clerk_flutter/test/clerk_flutter_test.dart

-13
This file was deleted.

0 commit comments

Comments
 (0)