Skip to content

Commit 8c6dd67

Browse files
committed
update
1 parent 198093e commit 8c6dd67

6 files changed

Lines changed: 110 additions & 12 deletions

File tree

flutter_app/lib/application/providers/auth_provider.dart

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@ import 'dart:async';
22
import 'dart:convert';
33
import 'dart:io';
44

5+
import 'package:flutter/foundation.dart';
56
import 'package:flutter/services.dart' show rootBundle;
7+
import 'package:flutter/widgets.dart' show AppLifecycleListener;
68
import 'package:flutter_riverpod/flutter_riverpod.dart';
7-
import 'package:supabase_flutter/supabase_flutter.dart';
9+
import 'package:supabase_flutter/supabase_flutter.dart' as supabase_flutter;
10+
import 'package:supabase_flutter/supabase_flutter.dart'
11+
show
12+
AuthChangeEvent,
13+
AuthException,
14+
OAuthProvider,
15+
Supabase,
16+
User;
817
import 'package:url_launcher/url_launcher.dart';
918

1019
import '../../core/config/supabase_config.dart';
@@ -27,6 +36,11 @@ class AuthState {
2736
});
2837
}
2938

39+
/// Sentinel returned by [AuthNotifier.signInWithOAuth] when the mobile
40+
/// deep-link callback never arrives after the user returns from the browser.
41+
/// The UI layer should replace this with a localised message.
42+
const oauthDeepLinkTimeout = '__OAUTH_DEEP_LINK_TIMEOUT__';
43+
3044
/// Manages Supabase auth state. When Supabase is not configured the notifier
3145
/// stays inert (state is always null) and the app runs fully offline.
3246
class AuthNotifier extends StateNotifier<AuthState?> {
@@ -48,6 +62,9 @@ class AuthNotifier extends StateNotifier<AuthState?> {
4862
}
4963

5064
// React to future auth changes (sign-in, sign-out, token refresh).
65+
// The onError handler is critical: without it, any error on the stream
66+
// (e.g. from a failed deep-link exchange) cancels the subscription and
67+
// the app can never detect sign-in / sign-out events again.
5168
_sub = client.auth.onAuthStateChange
5269
.map((data) {
5370
final user = data.session?.user;
@@ -61,7 +78,12 @@ class AuthNotifier extends StateNotifier<AuthState?> {
6178
createdAt: DateTime.tryParse(user.createdAt),
6279
);
6380
})
64-
.listen((authState) => state = authState);
81+
.listen(
82+
(authState) => state = authState,
83+
onError: (Object error, StackTrace stackTrace) {
84+
debugPrint('Auth stream error: $error');
85+
},
86+
);
6587
}
6688

6789
void _setFromUser(User user) {
@@ -118,6 +140,14 @@ class AuthNotifier extends StateNotifier<AuthState?> {
118140
/// Mobile: open browser with deep-link redirect.
119141
/// supabase_flutter intercepts the callback and exchanges the code
120142
/// for a session automatically via its built-in deep link handler.
143+
///
144+
/// Unlike the desktop flow (which blocks on a local HTTP server), the mobile
145+
/// flow must wait for the deep-link round-trip. We subscribe to
146+
/// [onAuthStateChange] and wait for a [signedIn] event or an error
147+
/// (e.g. PKCE exchange failure). When the app resumes from the browser, a
148+
/// short grace period is given for supabase_flutter to process the deep link.
149+
/// If the deep link never arrives (e.g. redirect URL not configured in
150+
/// the Supabase dashboard), the user gets a clear error message.
121151
Future<String?> _signInWithOAuthMobile(OAuthProvider provider) async {
122152
try {
123153
const redirectUrl = 'com.elymsyr.dungeonmastertool://auth-callback';
@@ -127,12 +157,55 @@ class AuthNotifier extends StateNotifier<AuthState?> {
127157
redirectTo: redirectUrl,
128158
);
129159

130-
await launchUrl(Uri.parse(res.url), mode: LaunchMode.externalApplication);
160+
// Listen for the auth result BEFORE launching the browser so we never
161+
// miss the event.
162+
final completer = Completer<String?>();
163+
164+
late final StreamSubscription<supabase_flutter.AuthState> authSub;
165+
authSub = Supabase.instance.client.auth.onAuthStateChange.listen(
166+
(data) {
167+
if (data.event == AuthChangeEvent.signedIn &&
168+
!completer.isCompleted) {
169+
authSub.cancel();
170+
completer.complete(null);
171+
}
172+
},
173+
onError: (Object error) {
174+
if (!completer.isCompleted) {
175+
authSub.cancel();
176+
completer.complete(
177+
error is AuthException ? error.message : error.toString(),
178+
);
179+
}
180+
},
181+
);
131182

132-
// supabase_flutter handles the deep link callback and session
133-
// exchange. The onAuthStateChange listener in _init() will
134-
// update the provider state and the UI will navigate to hub.
135-
return null;
183+
// When the app resumes from the browser, give supabase_flutter a
184+
// few seconds to receive and process the deep-link callback.
185+
// If nothing arrives, surface an actionable error.
186+
AppLifecycleListener? lifecycleListener;
187+
lifecycleListener = AppLifecycleListener(
188+
onResume: () {
189+
lifecycleListener?.dispose();
190+
lifecycleListener = null;
191+
Future<void>.delayed(const Duration(seconds: 8), () {
192+
if (!completer.isCompleted) {
193+
authSub.cancel();
194+
completer.complete(oauthDeepLinkTimeout);
195+
}
196+
});
197+
},
198+
);
199+
200+
await launchUrl(
201+
Uri.parse(res.url), mode: LaunchMode.externalApplication);
202+
203+
final result = await completer.future;
204+
205+
// Clean up in case auth succeeded before resume.
206+
lifecycleListener?.dispose();
207+
208+
return result;
136209
} on AuthException catch (e) {
137210
return e.message;
138211
} catch (e) {

flutter_app/lib/presentation/l10n/app_de.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,9 @@
7979
"importConfirmTitle": "\"{name}\" importieren?",
8080
"@importConfirmTitle": { "placeholders": { "name": { "type": "String" } } },
8181
"importConfirmBody": "Dies fügt {count} Einträge zu Ihrer Welt hinzu.",
82-
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } }
82+
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } },
83+
"oauthSignInFailed": "Anmeldung konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut oder stellen Sie sicher, dass die Redirect-URL in Ihrem Supabase-Projekt konfiguriert ist.\n(Redirect-URL: {redirectUrl})",
84+
"@oauthSignInFailed": { "placeholders": { "redirectUrl": { "type": "String" } } },
85+
"authStreamError": "Authentifizierungsfehler: {error}",
86+
"@authStreamError": { "placeholders": { "error": { "type": "String" } } }
8387
}

flutter_app/lib/presentation/l10n/app_en.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,9 @@
7979
"importConfirmTitle": "Import \"{name}\"?",
8080
"@importConfirmTitle": { "placeholders": { "name": { "type": "String" } } },
8181
"importConfirmBody": "This will add {count} entities to your world.",
82-
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } }
82+
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } },
83+
"oauthSignInFailed": "Sign-in could not be completed. Please try again or ensure the redirect URL is configured in your Supabase project.\n(Redirect URL: {redirectUrl})",
84+
"@oauthSignInFailed": { "placeholders": { "redirectUrl": { "type": "String" } } },
85+
"authStreamError": "Authentication error: {error}",
86+
"@authStreamError": { "placeholders": { "error": { "type": "String" } } }
8387
}

flutter_app/lib/presentation/l10n/app_fr.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,9 @@
7979
"importConfirmTitle": "Importer \"{name}\" ?",
8080
"@importConfirmTitle": { "placeholders": { "name": { "type": "String" } } },
8181
"importConfirmBody": "Cela ajoutera {count} entités à votre monde.",
82-
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } }
82+
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } },
83+
"oauthSignInFailed": "La connexion n'a pas pu être effectuée. Veuillez réessayer ou vérifier que l'URL de redirection est configurée dans votre projet Supabase.\n(URL de redirection : {redirectUrl})",
84+
"@oauthSignInFailed": { "placeholders": { "redirectUrl": { "type": "String" } } },
85+
"authStreamError": "Erreur d'authentification : {error}",
86+
"@authStreamError": { "placeholders": { "error": { "type": "String" } } }
8387
}

flutter_app/lib/presentation/l10n/app_tr.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,9 @@
7979
"importConfirmTitle": "\"{name}\" içe aktarılsın mı?",
8080
"@importConfirmTitle": { "placeholders": { "name": { "type": "String" } } },
8181
"importConfirmBody": "Dünyanıza {count} varlık eklenecek.",
82-
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } }
82+
"@importConfirmBody": { "placeholders": { "count": { "type": "int" } } },
83+
"oauthSignInFailed": "Giriş tamamlanamadı. Lütfen tekrar deneyin veya Supabase projesinde redirect URL'in tanımlı olduğundan emin olun.\n(Redirect URL: {redirectUrl})",
84+
"@oauthSignInFailed": { "placeholders": { "redirectUrl": { "type": "String" } } },
85+
"authStreamError": "Kimlik doğrulama hatası: {error}",
86+
"@authStreamError": { "placeholders": { "error": { "type": "String" } } }
8387
}

flutter_app/lib/presentation/screens/landing/landing_screen.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:supabase_flutter/supabase_flutter.dart';
55

66
import '../../../application/providers/auth_provider.dart';
77
import '../../../core/config/supabase_config.dart';
8+
import '../../l10n/app_localizations.dart';
89
import '../../theme/dm_tool_colors.dart';
910

1011
class LandingScreen extends ConsumerStatefulWidget {
@@ -450,7 +451,15 @@ class _LandingScreenState extends ConsumerState<LandingScreen> {
450451
_info = null;
451452
});
452453

453-
final error = await ref.read(authProvider.notifier).signInWithOAuth(provider);
454+
var error = await ref.read(authProvider.notifier).signInWithOAuth(provider);
455+
456+
// Replace sentinel with localised message.
457+
if (error == oauthDeepLinkTimeout && mounted) {
458+
final l10n = L10n.of(context);
459+
error = l10n?.oauthSignInFailed(
460+
'com.elymsyr.dungeonmastertool://auth-callback') ??
461+
error;
462+
}
454463

455464
if (mounted) {
456465
setState(() {

0 commit comments

Comments
 (0)