Skip to content

Commit 611336e

Browse files
committed
1
1 parent afc2306 commit 611336e

3 files changed

Lines changed: 133 additions & 21 deletions

File tree

lib/services/auth/sectl_auth_service.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class SectlAuthService {
5353
AuthLoopbackServer? _loopbackServer;
5454
Completer<UserInfo>? _activeLoginCompleter;
5555
WebAuthPopupSession? _webPopupSession;
56+
bool _webRedirectFallbackTriggered = false;
5657

5758
static String generateCodeVerifier({int byteLength = 32}) {
5859
final random = Random.secure();
@@ -141,6 +142,7 @@ class SectlAuthService {
141142

142143
final authUrl = getAuthorizationUrl(session);
143144
_activeLoginCompleter = Completer<UserInfo>();
145+
_webRedirectFallbackTriggered = false;
144146

145147
if (targetPlatform == PendingAuthTargetPlatform.web) {
146148
final popupSession = await openWebAuthPopup(authUrl);
@@ -493,6 +495,10 @@ class SectlAuthService {
493495
_activeLoginCompleter!.complete(userInfo);
494496
}
495497
} catch (error, stackTrace) {
498+
final redirected = await _fallbackWebPopupToFullRedirect(error);
499+
if (redirected) {
500+
return;
501+
}
496502
if (_activeLoginCompleter != null &&
497503
!_activeLoginCompleter!.isCompleted) {
498504
_activeLoginCompleter!.completeError(error, stackTrace);
@@ -506,6 +512,29 @@ class SectlAuthService {
506512
}
507513
}
508514

515+
Future<bool> _fallbackWebPopupToFullRedirect(Object error) async {
516+
if (!kIsWeb || _webRedirectFallbackTriggered) {
517+
return false;
518+
}
519+
520+
if (error is! StateError ||
521+
!error.message.contains('popup was closed before finishing')) {
522+
return false;
523+
}
524+
525+
final pendingSession = await _tokenManager.getPendingAuthSession();
526+
if (pendingSession == null ||
527+
pendingSession.targetPlatform != PendingAuthTargetPlatform.web ||
528+
pendingSession.isExpired) {
529+
return false;
530+
}
531+
532+
_webRedirectFallbackTriggered = true;
533+
final authUrl = getAuthorizationUrl(pendingSession);
534+
await navigateBrowserTo(authUrl);
535+
return true;
536+
}
537+
509538
PendingAuthTargetPlatform _resolveTargetPlatform() {
510539
if (kIsWeb) {
511540
return PendingAuthTargetPlatform.web;
@@ -592,6 +621,7 @@ class SectlAuthService {
592621
_loopbackServer = null;
593622
await _webPopupSession?.close();
594623
_webPopupSession = null;
624+
_webRedirectFallbackTriggered = false;
595625
if (clearCompleter) {
596626
_activeLoginCompleter = null;
597627
}
@@ -605,6 +635,7 @@ class SectlAuthService {
605635
_linkSubscription = null;
606636
_loopbackServer = null;
607637
_webPopupSession = null;
638+
_webRedirectFallbackTriggered = false;
608639
_activeLoginCompleter = null;
609640
}
610641
}

lib/services/auth/web_popup_auth_web.dart

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:js_interop';
24

35
import 'package:universal_html/html.dart' as html;
46

7+
extension on JSObject {
8+
external JSAny? operator [](String key);
9+
}
10+
511
class WebAuthPopupSession {
612
WebAuthPopupSession._(this._popupWindow) {
713
_messageSubscription = html.window.onMessage.listen((event) {
8-
final data = event.data;
9-
if (data is! Map) return;
14+
final payload = _normalizeMessagePayload(event.data);
15+
if (payload == null) {
16+
return;
17+
}
18+
19+
final type = payload['type'];
20+
final href = payload['href'];
1021

11-
final type = data['type'];
12-
final href = data['href'];
1322
if (type != 'sectl-auth-callback' || href is! String) {
1423
return;
1524
}
1625

17-
final expectedOrigin = html.window.location.origin;
18-
if (event.origin.isNotEmpty &&
19-
expectedOrigin.isNotEmpty &&
20-
event.origin != expectedOrigin) {
26+
if (href.isEmpty) {
2127
return;
2228
}
2329

@@ -27,15 +33,78 @@ class WebAuthPopupSession {
2733
unawaited(close());
2834
});
2935

36+
Future.delayed(const Duration(milliseconds: 500), () {
37+
if (_callbackCompleter.isCompleted) return;
38+
_startClosedTimer();
39+
});
40+
}
41+
42+
final html.WindowBase _popupWindow;
43+
final Completer<Uri> _callbackCompleter = Completer<Uri>();
44+
StreamSubscription<html.MessageEvent>? _messageSubscription;
45+
Timer? _closedTimer;
46+
47+
Future<Uri> waitForCallback() => _callbackCompleter.future;
48+
49+
Map<String, dynamic>? _normalizeMessagePayload(dynamic data) {
50+
if (data is String) {
51+
try {
52+
final decoded = jsonDecode(data);
53+
if (decoded is Map<String, dynamic>) {
54+
return decoded;
55+
}
56+
if (decoded is Map) {
57+
return decoded.map(
58+
(key, value) => MapEntry(key.toString(), value),
59+
);
60+
}
61+
} catch (_) {
62+
return null;
63+
}
64+
}
65+
66+
if (data is Map<String, dynamic>) {
67+
return data;
68+
}
69+
70+
if (data is Map) {
71+
return data.map(
72+
(key, value) => MapEntry(key.toString(), value),
73+
);
74+
}
75+
76+
try {
77+
final object = data as JSObject;
78+
final type = object['type'];
79+
final href = object['href'];
80+
if (type == null || href == null) {
81+
return null;
82+
}
83+
return {
84+
'type': (type as JSString).toDart,
85+
'href': (href as JSString).toDart,
86+
};
87+
} catch (_) {
88+
return null;
89+
}
90+
}
91+
92+
void _startClosedTimer() {
3093
_closedTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
94+
if (_callbackCompleter.isCompleted) {
95+
_closedTimer?.cancel();
96+
return;
97+
}
98+
3199
final callbackUri = _tryReadPopupCallbackUri();
32100
if (callbackUri != null && !_callbackCompleter.isCompleted) {
33101
_callbackCompleter.complete(callbackUri);
34102
unawaited(close());
35103
return;
36104
}
37105

38-
if (_popupWindow.closed == true && !_callbackCompleter.isCompleted) {
106+
final isClosed = _isPopupClosed();
107+
if (isClosed && !_callbackCompleter.isCompleted) {
39108
_callbackCompleter.completeError(
40109
StateError('The SECTL login popup was closed before finishing.'),
41110
);
@@ -44,21 +113,30 @@ class WebAuthPopupSession {
44113
});
45114
}
46115

47-
final html.WindowBase _popupWindow;
48-
final Completer<Uri> _callbackCompleter = Completer<Uri>();
49-
StreamSubscription<html.MessageEvent>? _messageSubscription;
50-
Timer? _closedTimer;
51-
52-
Future<Uri> waitForCallback() => _callbackCompleter.future;
116+
bool _isPopupClosed() {
117+
try {
118+
final popupJs = _popupWindow as JSObject;
119+
final closed = popupJs['closed'];
120+
return (closed as JSBoolean).toDart;
121+
} catch (_) {
122+
return false;
123+
}
124+
}
53125

54126
Uri? _tryReadPopupCallbackUri() {
55127
try {
56-
final href = _popupWindow.location.href;
57-
if (href == null || href.isEmpty) {
128+
final location = _popupWindow.location as JSObject;
129+
final href = location['href'];
130+
if (href == null) {
131+
return null;
132+
}
133+
134+
final hrefString = (href as JSString).toDart;
135+
if (hrefString.isEmpty) {
58136
return null;
59137
}
60138

61-
final uri = Uri.parse(href);
139+
final uri = Uri.parse(hrefString);
62140
if (uri.queryParameters.containsKey('code') ||
63141
uri.queryParameters.containsKey('error')) {
64142
return uri;
@@ -107,5 +185,8 @@ Future<WebAuthPopupSession?> openWebAuthPopup(String url) async {
107185
].join(',');
108186

109187
final popup = html.window.open(url, 'sectl_auth_popup', features);
188+
if (popup == null) {
189+
return null;
190+
}
110191
return WebAuthPopupSession._(popup);
111192
}

web/auth_callback.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,10 @@ <h1 id="title">Completing SECTL sign-in</h1>
137137

138138
const target = new URL(targetUrl);
139139
window.opener.postMessage(
140-
{
140+
JSON.stringify({
141141
type: 'sectl-auth-callback',
142142
href: target.toString()
143-
},
143+
}),
144144
target.origin,
145145
);
146146
return true;
@@ -192,7 +192,7 @@ <h1 id="title">Completing SECTL sign-in</h1>
192192

193193
setTimeout(() => {
194194
if (maybeNotifyOpener(payload, targetUrl)) {
195-
window.close();
195+
setTimeout(() => window.close(), 150);
196196
return;
197197
}
198198
window.location.assign(targetUrl);

0 commit comments

Comments
 (0)