Skip to content

Commit

Permalink
Merge pull request #2346 from airqo-platform/null-check-operator-fix
Browse files Browse the repository at this point in the history
Enhance authentication flow with loading indicators and error handlin…
  • Loading branch information
Baalmart authored Dec 15, 2024
2 parents 9a82558 + 47bb227 commit 5e6bc96
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 37 deletions.
65 changes: 53 additions & 12 deletions mobile-v3/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ import 'package:path_provider/path_provider.dart';
import 'package:airqo/src/app/shared/pages/no_internet_banner.dart';
import 'package:loggy/loggy.dart';
import 'src/app/shared/repository/hive_repository.dart';
import 'package:jwt_decoder/jwt_decoder.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();

Loggy.initLoggy(
logPrinter: const PrettyPrinter(),
Expand Down Expand Up @@ -157,21 +159,46 @@ class _DeciderState extends State<Decider> {
logDebug('Current connectivity state: $connectivityState');
return Stack(
children: [
FutureBuilder(
FutureBuilder<String?>(
future: HiveRepository.getData('token', HiveBoxNames.authBox),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (!snapshot.hasData) {
logInfo('No authentication token found. Navigating to WelcomeScreen');
return WelcomeScreen();
} else {
logInfo('Authentication token found. Navigating to NavPage');
return NavPage();
}
} else {
logError('Error loading authentication state');
// Handle loading state
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

// Handle errors
if (snapshot.hasError) {
logError(
'Error loading authentication state: ${snapshot.error}');
return Scaffold(
body: Center(child: Text('An Error occurred.')));
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Unable to verify authentication status'),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => setState(() {}),
child: Text('Retry'),
),
],
),
),
);
}

// Check if token exists and is not null
final token = snapshot.data;
if (!isValidToken(token)) {
logInfo(
'No authentication token found. Navigating to WelcomeScreen');
return WelcomeScreen();
} else {
logInfo('Authentication token found. Navigating to NavPage');
return NavPage();
}
},
),
Expand All @@ -195,3 +222,17 @@ class _DeciderState extends State<Decider> {
);
}
}

bool isValidToken(String? token) {
if (token == null || token.isEmpty) return false;
try {
if (JwtDecoder.isExpired(token)) {
logInfo('Token has expired');
return false;
}
return true;
} catch (e) {
logError('Token validation failed', e, StackTrace.current);
return false;
}
}
17 changes: 12 additions & 5 deletions mobile-v3/lib/src/app/auth/pages/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:airqo/src/meta/utils/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:loggy/loggy.dart';

class LoginPage extends StatefulWidget {
const LoginPage({super.key});
Expand All @@ -28,8 +29,12 @@ class _LoginPageState extends State<LoginPage> {

@override
void initState() {
authBloc = context.read<AuthBloc>();
super.initState();
try {
authBloc = context.read<AuthBloc>();
} catch (e) {
logError('Failed to initialize AuthBloc: $e');
}
}

@override
Expand Down Expand Up @@ -82,7 +87,7 @@ class _LoginPageState extends State<LoginPage> {
),
),
validator: (value) {
if (value!.length == 0) {
if (value == null || value.isEmpty) {
return "This field cannot be blank.";
}
return null;
Expand All @@ -100,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
),
),
validator: (value) {
if (value!.length == 0) {
if (value == null || value.isEmpty) {
return "This field cannot be blank.";
}
return null;
Expand Down Expand Up @@ -130,8 +135,10 @@ class _LoginPageState extends State<LoginPage> {
onTap: loading
? null
: () {
if (formKey.currentState!.validate()) {
authBloc!.add(LoginUser(
final currentForm = formKey.currentState;
if (currentForm != null &&
currentForm.validate()) {
authBloc?.add(LoginUser(
emailController.text.trim(),
passwordController.text.trim()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ThemeImpl extends ThemeRepository {
await HiveRepository.saveData("themeBox", "theme", "light");
}

String newTheme = await HiveRepository.getData("theme", "themeBox");
String newTheme = await HiveRepository.getData("theme", "themeBox") ?? "light";
return newTheme;
}

Expand Down
8 changes: 5 additions & 3 deletions mobile-v3/lib/src/app/profile/repository/user_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ abstract class UserRepository extends BaseRepository {
class UserImpl extends UserRepository {
@override
Future<ProfileResponseModel> loadUserProfile() async {
String userId =
await HiveRepository.getData("userId", HiveBoxNames.authBox);
final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox);

if (userId == null) {
throw Exception("User ID not found");
}

Response profileResponse =
await createAuthenticatedGetRequest("/api/v2/users/${userId}", {});

ProfileResponseModel model =
profileResponseModelFromJson(profileResponse.body);

return model;
}
}
49 changes: 39 additions & 10 deletions mobile-v3/lib/src/app/shared/repository/base_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@ import 'package:http/http.dart' as http;
import 'package:http/http.dart';

class BaseRepository {
String? _cachedToken;

Future<String?> _getToken() async {
if (_cachedToken == null) {
_cachedToken = await HiveRepository.getData("token", HiveBoxNames.authBox);
}
return _cachedToken;
}

Future<Response> createPostRequest(
{required String path, dynamic data}) async {
String? token = await HiveRepository.getData("token", HiveBoxNames.authBox);
final token = await _getToken();
if (token == null) {
throw Exception('Authentication token not found');
}

String url = ApiUtils.baseUrl + path;

Expand All @@ -16,43 +28,61 @@ class BaseRepository {
Response response = await http.post(Uri.parse(url),
body: json.encode(data),
headers: {
"Authorization": "${token}",
"Authorization": "Bearer ${token}",
"Accept": "*/*",
"contentType": "application/json"
});

print(response.statusCode);

if (response.statusCode != 200) {
throw new Exception(json.decode(response.body)['message']);
final responseBody = json.decode(response.body);
final errorMessage = responseBody is Map && responseBody.containsKey('message')
? responseBody['message']
: 'An error occurred';
throw new Exception(errorMessage);
}
return response;
}

Future<Response> createGetRequest(
String path, Map<String, String> queryParams) async {
// String token = await HiveRepository.getData("token", HiveBoxNames.authBox);
String? token = await _getToken();

String url = ApiUtils.baseUrl + path;

Response response = await http
.get(Uri.parse(url).replace(queryParameters: queryParams), headers: {
Map<String, String> headers = {
"Accept": "*/*",
"Content-Type": "application/json",
});
};

if (token != null) {
headers["Authorization"] = "Bearer $token";
}

Response response = await http.get(
Uri.parse(url).replace(queryParameters: queryParams),
headers: headers);

print(response.statusCode);

if (response.statusCode != 200) {
throw new Exception(json.decode(response.body)['message']);
final responseBody = json.decode(response.body);
final errorMessage = responseBody is Map && responseBody.containsKey('message')
? responseBody['message']
: 'An error occurred';
throw new Exception(errorMessage);
}

return response;
}

Future<Response> createAuthenticatedGetRequest(
String path, Map<String, String> queryParams) async {
String token = await HiveRepository.getData("token", HiveBoxNames.authBox);
String? token = await _getToken();
if (token == null) {
throw Exception('Authentication token not found');
}

print(token);

Expand All @@ -65,7 +95,6 @@ class BaseRepository {
"Content-Type": "application/json",
});


if (response.statusCode != 200) {
throw new Exception(json.decode(response.body)['message']);
}
Expand Down
16 changes: 10 additions & 6 deletions mobile-v3/lib/src/app/shared/repository/hive_repository.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:hive/hive.dart';
import 'package:loggy/loggy.dart';

class HiveBoxNames {
const HiveBoxNames._();
Expand All @@ -14,12 +15,15 @@ class HiveRepository {
await box.put(key, value);
}

static Future<dynamic>? getData(String key, String boxName) async {
var box = await Hive.openBox(boxName);

var value = box.get(key);

return value;
static Future<String?> getData(String key, String boxName) async {
try {
var box = await Hive.openBox(boxName);
var value = box.get(key);
return value is String ? value : null;
} catch (e) {
logError('Error retrieving data from Hive: $e');
return null;
}
}

static Future<void>? deleteData(String boxName, String key) async {
Expand Down
8 changes: 8 additions & 0 deletions mobile-v3/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.9.0"
jwt_decoder:
dependency: "direct main"
description:
name: jwt_decoder
sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions mobile-v3/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ dependencies:
logger: ^2.5.0
connectivity_plus: ^6.1.0
flutter_loggy: ^2.0.3+1
jwt_decoder: ^2.0.1

dev_dependencies:
flutter_test:
Expand Down

0 comments on commit 5e6bc96

Please sign in to comment.