Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application
android:label="Elephant" android:name="${applicationName}"
android:label="Elephant"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">

Expand All @@ -17,34 +18,56 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->

<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="elephant"
android:host="oauth-callback" />
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="elephant.commandlinecoding.in"
android:pathPrefix="/api/auth/callback" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>

<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
</queries>
</manifest>
97 changes: 97 additions & 0 deletions mobile/lib/controllers/auth.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:app_links/app_links.dart';
import 'package:mobile/models/user.dart';
import '../core/constants.dart';
import '../services/auth.dart';

class AuthState extends ChangeNotifier {
final AuthService _authService = AuthService();
final AppLinks _appLinks = AppLinks();
StreamSubscription<Uri>? _linkSubscription;

String? _token;
bool _isLoading = false;
Expand All @@ -23,6 +29,33 @@ class AuthState extends ChangeNotifier {

AuthState() {
onGlobalUnauthorized = logoutSilently;
_initDeepLinks();
}

Future<void> _initDeepLinks() async {
try {
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
_handleIncomingUri(initialUri);
}
} catch (e) {
debugPrint("Error reading initial deep link: $e");
}

_linkSubscription = _appLinks.uriLinkStream.listen(
(Uri uri) {
_handleIncomingUri(uri);
},
onError: (err) {
debugPrint("Deep link stream error: $err");
},
);
}

void _handleIncomingUri(Uri uri) {
if (uri.scheme == 'elephant' && uri.host == 'oauth-callback') {
handleOAuthCallback(uri);
}
}

Future<void> loadUserProfile() async {
Expand Down Expand Up @@ -147,6 +180,64 @@ class AuthState extends ChangeNotifier {
return false;
}

Future<void> handleOAuthLogin(String provider) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();

try {
final String oAuthUrl = "${Env.httpBaseUrl}/auth/$provider";
final Uri uri = Uri.parse(oAuthUrl);

final bool launched = await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);

if (!launched) {
_errorMessage =
"Could not launch web browser for $provider authentication.";
}
} catch (e) {
_errorMessage = "OAuth launch error: $e";
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<bool> handleOAuthCallback(Uri uri) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();

try {
final accessToken =
uri.queryParameters['access_token'] ?? uri.queryParameters['token'];
final refreshToken = uri.queryParameters['refresh_token'];

if (accessToken != null) {
_token = accessToken;
await _authService.saveTokens(accessToken, refreshToken ?? accessToken);
await loadUserProfile();

_isLoading = false;
notifyListeners();
return true;
} else if (uri.queryParameters.containsKey('error')) {
_errorMessage = uri.queryParameters['error'];
} else {
_errorMessage = "OAuth callback received no valid token payload.";
}
} catch (e) {
_errorMessage = "Failed to parse authentication callback data.";
}

_isLoading = false;
notifyListeners();
return false;
}

Future<void> logout() async {
_token = null;
_currentUser = null;
Expand All @@ -162,4 +253,10 @@ class AuthState extends ChangeNotifier {
notifyListeners();
}
}

@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
}
24 changes: 15 additions & 9 deletions mobile/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import 'pages/home_page.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Future.wait([Env.init(), AuthService().initTokens()]);
await Env.init();
await AuthService().initTokens();

final themeProvider = ThemeProvider();

// while (!themeProvider.isInitialized) {
// await Future.delayed(const Duration(milliseconds: 10));
// }
while (!themeProvider.isInitialized) {
await Future.delayed(const Duration(milliseconds: 10));
}

runApp(
MultiProvider(
Expand Down Expand Up @@ -55,23 +56,29 @@ class SessionGateway extends StatefulWidget {
State<SessionGateway> createState() => _SessionGatewayState();
}

class _SessionGatewayState extends State<SessionGateway> {
class _SessionGatewayState extends State<SessionGateway>
with WidgetsBindingObserver {
bool _hasCheckedAutoLogin = false;
String? _lastInitializedToken;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_performInitialAutoLoginCheck();
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

void _performInitialAutoLoginCheck() async {
final auth = context.read<AuthState>();
final token = await auth.checkAutoLogin();

if (!mounted) return;

if (token != null) {
if (token != null && mounted) {
_lastInitializedToken = token;
context.read<ChatController>().initSession();
}
Expand All @@ -91,7 +98,6 @@ class _SessionGatewayState extends State<SessionGateway> {
body: Center(
child: CircularProgressIndicator(
color: Theme.of(context).colorScheme.primary,
strokeWidth: 3,
),
),
);
Expand Down
Loading
Loading