diff --git a/.env b/.env
index a376873..c89bd06 100644
--- a/.env
+++ b/.env
@@ -2,8 +2,11 @@
# API_URL=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io/
# API_URL=https://node.shoy.publicvm.com/
API_URL=https://node.shoy.publicvm.com/
-# API_URL=https://0ec88db618e2.ngrok-free.app/
-# API_URL=https://67ee79b6365d.ngrok-free.app/
+API_test_URL=https://example.com/
+# API_URL=https://wanita-hypernormal-cherise.ngrok-free.dev/
+# API_URL=https://avah-pollinical-randal.ngrok-free.dev/
+# API_URL=https://0f9eef01f130.ngrok-free.app/
+# API_URL=https://ingeborg-untrammed-leo.ngrok-free.dev/
# Socket_Url=https://app-dbef67eb-9a2e-44fa-abff-3e8b83204d9c.cleverapps.io
# serverClientId=1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com
diff --git a/Dockerfile b/Dockerfile
index b0e2c25..448ee04 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,23 +1,43 @@
-# Dockerfile.ci
+# ---------------- BASE STAGE ----------------
FROM ghcr.io/cirruslabs/flutter:3.35.5 AS base
-
WORKDIR /app
-# copy pubspec first so we can cache dependencies
+# Copy pubspec first for dependency caching
COPY pubspec.* ./
RUN flutter pub get
-# copy source
+# Copy source code
COPY . .
-# LINTING
-FROM base AS lint
+# ---------------- TEST STAGE ----------------
+FROM base AS test
+WORKDIR /app
+
+# Run unit/widget tests
+RUN flutter test --no-pub
+
+# ---------------- LINT STAGE ----------------
+FROM ghcr.io/cirruslabs/flutter:3.35.5 AS lint
+WORKDIR /app
+
+COPY pubspec.* ./
+RUN flutter pub get
+
+COPY . .
RUN flutter analyze
-# UNIT TETSING
-FROM base AS test
-RUN flutter test
-# BUILDING
-FROM test AS build-apk
-RUN flutter build apk --release
\ No newline at end of file
+# ---------------- BUILD APK STAGE ----------------
+FROM base AS build-apk
+
+# Install Android SDK components early for caching
+RUN yes | sdkmanager --licenses
+
+RUN sdkmanager \
+ "platform-tools" \
+ "platforms;android-34" \
+ "build-tools;34.0.0" \
+ "cmdline-tools;latest"
+
+# Now build release APK
+RUN flutter build apk --release
diff --git a/Jenkinsfile b/Jenkinsfile
index 9199650..113ba89 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -96,22 +96,7 @@ EOF
}
}
- stage('Kaniko: Test image') {
- steps {
- container('kaniko') {
- script {
- sh '''
- echo "Kaniko building test target (no push)..."
- /kaniko/executor \
- --context=. \
- --dockerfile=Dockerfile \
- --no-push \
- --target=test
- '''
- }
- }
- }
- }
+
stage('Kaniko: Build-APK image (build & push)') {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fe9021e..ce5c0d5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -14,6 +14,7 @@
android:usesCleartextTraffic="true">
-
+
diff --git a/lib/core/classes/PickedImage.dart b/lib/core/classes/PickedImage.dart
index 3f2ba6a..88185e1 100644
--- a/lib/core/classes/PickedImage.dart
+++ b/lib/core/classes/PickedImage.dart
@@ -10,10 +10,10 @@ class PickedImage {
PickedImage({this.file, this.bytes, required this.name, this.path});
}
-Future pickImage() async {
+Future pickImage({ImagePicker? picker}) async {
try {
- final ImagePicker picker = ImagePicker();
- final XFile? image = await picker.pickImage(source: ImageSource.gallery);
+ final ImagePicker _picker = picker ?? ImagePicker();
+ final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
if (image != null) {
return PickedImage(
@@ -28,10 +28,13 @@ Future pickImage() async {
}
}
-Future> pickImages({int maxImages = 4}) async {
+Future> pickImages({
+ int maxImages = 4,
+ ImagePicker? picker,
+}) async {
try {
- final ImagePicker picker = ImagePicker();
- final XFile? image = await picker.pickImage(source: ImageSource.gallery);
+ final ImagePicker _picker = picker ?? ImagePicker();
+ final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
if (image == null) return [];
@@ -43,10 +46,10 @@ Future> pickImages({int maxImages = 4}) async {
}
}
-Future pickVideo() async {
+Future pickVideo({ImagePicker? picker}) async {
try {
- final ImagePicker picker = ImagePicker();
- final XFile? video = await picker.pickVideo(source: ImageSource.gallery);
+ final ImagePicker _picker = picker ?? ImagePicker();
+ final XFile? video = await _picker.pickVideo(source: ImageSource.gallery);
if (video != null) {
return PickedImage(
diff --git a/lib/core/models/usermodel.dart b/lib/core/models/usermodel.dart
index 7c7993e..d25e977 100644
--- a/lib/core/models/usermodel.dart
+++ b/lib/core/models/usermodel.dart
@@ -38,7 +38,8 @@ class UserModel {
final bool? tfaVerified;
@HiveField(10)
- final Set interests;
+ final Set interests; // stores only categories names og selected
+
@HiveField(11)
final String? localProfilePhotoPath; // path of local profile photo
diff --git a/lib/core/providers/dio_interceptor.dart b/lib/core/providers/dio_interceptor.dart
index 7308fd2..8875429 100644
--- a/lib/core/providers/dio_interceptor.dart
+++ b/lib/core/providers/dio_interceptor.dart
@@ -205,6 +205,8 @@ class AuthInterceptor extends Interceptor {
);
await authLocalRepository.saveTokens(newTokens);
+ await Future.delayed(const Duration(milliseconds: 5000)); //
+
print("AuthInterceptor: Token refreshed successfully");
return true;
} on DioException catch (e) {
diff --git a/lib/core/services/deep_link_service.dart b/lib/core/services/deep_link_service.dart
index 59ab16b..1436c8a 100644
--- a/lib/core/services/deep_link_service.dart
+++ b/lib/core/services/deep_link_service.dart
@@ -1,6 +1,7 @@
// ignore_for_file: unnecessary_null_comparison
import 'dart:async';
+import 'dart:developer';
import 'package:app_links/app_links.dart';
import 'package:flutter/widgets.dart';
@@ -16,7 +17,10 @@ class DeepLinkService {
static void init() {
_appLinks.uriLinkStream.listen((uri) {
+ log("🔵 DeepLinkService received URI: $uri");
+
if (uri != null && _completer != null && !_completer!.isCompleted) {
+ log("🟢 Completing deep link future with: $uri");
_completer!.complete(uri);
}
});
diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart
index a046466..b1f35e2 100644
--- a/lib/core/theme/app_theme.dart
+++ b/lib/core/theme/app_theme.dart
@@ -1,4 +1,3 @@
-// ignore_for_file: deprecated_member_use
import 'package:flutter/material.dart';
import 'Palette.dart';
diff --git a/lib/features/auth/models/ExploreCategory.dart b/lib/features/auth/models/ExploreCategory.dart
new file mode 100644
index 0000000..782ac43
--- /dev/null
+++ b/lib/features/auth/models/ExploreCategory.dart
@@ -0,0 +1,10 @@
+class ExploreCategory {
+ final String id;
+ final String name;
+
+ ExploreCategory({required this.id, required this.name});
+
+ factory ExploreCategory.fromMap(Map map) {
+ return ExploreCategory(id: map["id"], name: map["name"]);
+ }
+}
diff --git a/lib/features/auth/repositories/auth_local_repository.dart b/lib/features/auth/repositories/auth_local_repository.dart
index b8a6645..cd04252 100644
--- a/lib/features/auth/repositories/auth_local_repository.dart
+++ b/lib/features/auth/repositories/auth_local_repository.dart
@@ -82,4 +82,8 @@ class AuthLocalRepository {
]);
_tokenStreamController.add(null);
}
+
+ void dispose() {
+ _tokenStreamController.close();
+ }
}
diff --git a/lib/features/auth/repositories/auth_remote_repository.dart b/lib/features/auth/repositories/auth_remote_repository.dart
index 8474d8c..7cfeafa 100644
--- a/lib/features/auth/repositories/auth_remote_repository.dart
+++ b/lib/features/auth/repositories/auth_remote_repository.dart
@@ -11,6 +11,7 @@ import 'package:lite_x/core/models/TokensModel.dart';
import 'package:lite_x/core/models/usermodel.dart';
import 'package:lite_x/core/providers/dio_interceptor.dart';
import 'package:lite_x/core/services/deep_link_service.dart';
+import 'package:lite_x/features/auth/models/ExploreCategory.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:dio/dio.dart';
@@ -27,11 +28,12 @@ class AuthRemoteRepository {
final Dio _dio;
AuthRemoteRepository({required Dio dio}) : _dio = dio;
//---------------------------------------------------github------------------------------------------------------//
- Future> loginWithGithub() async {
+
+ Future>
+ loginWithGithub() async {
try {
final baseUrl = dotenv.env["API_URL"]!;
final authUrl = "${baseUrl}oauth2/authorize/github";
-
final opened = await launchUrl(
Uri.parse(authUrl),
mode: LaunchMode.externalApplication,
@@ -40,7 +42,6 @@ class AuthRemoteRepository {
if (!opened) {
return left(AppFailure(message: "Could not open browser"));
}
-
final uri = await DeepLinkService.waitForLink();
if (uri == null) {
@@ -56,7 +57,9 @@ class AuthRemoteRepository {
}
final decodedUser = Uri.decodeComponent(userRaw);
- final user = UserModel.fromJson(decodedUser);
+ final Map userJson = jsonDecode(decodedUser);
+ final bool newuser = userJson["newuser"];
+ final user = UserModel.fromMap(userJson);
final tokens = TokensModel(
accessToken: token,
@@ -65,20 +68,21 @@ class AuthRemoteRepository {
refreshTokenExpiry: DateTime.now().add(const Duration(days: 30)),
);
- return right((user, tokens));
+ return right((user, tokens, newuser));
} catch (e) {
return left(AppFailure(message: e.toString()));
}
}
+
//--------------------------------------------------------google-------------------------------------------------------------------//
final _googleSignIn = signIn.GoogleSignIn(
serverClientId:
- "https://1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com",
+ "1096363232606-2fducjadk56bt4nsreqkj2jna7oiomga.apps.googleusercontent.com",
scopes: ['email', 'https://www.googleapis.com/auth/userinfo.profile'],
);
- Future>
+ Future>
signInWithGoogleAndroid() async {
try {
final String apiUrl = dotenv.env["API_URL"]!;
@@ -87,10 +91,14 @@ class AuthRemoteRepository {
if (googleUser == null) {
return left(AppFailure(message: "Google login canceled"));
}
-
final googleAuth = await googleUser.authentication;
- final idToken = googleAuth.idToken;
+ final email = googleUser.email;
+ print("GOOGLE EMAIL = $email\n");
+
+ // final existsResult = await check_email(email: email);
+ // final exists = existsResult.fold((_) => false, (v) => v);
+ final idToken = googleAuth.idToken;
debugPrint("GOOGLE ID TOKEN = $idToken");
final resp = await http.post(
@@ -112,13 +120,55 @@ class AuthRemoteRepository {
accessTokenExpiry: DateTime.now().add(const Duration(hours: 1)),
refreshTokenExpiry: DateTime.now().add(const Duration(days: 30)),
);
+ final newuser = data["user"]["newuser"]; //
- return right((user, tokens));
+ return right((user, tokens, newuser));
} catch (e) {
return left(AppFailure(message: e.toString()));
}
}
+ //-------------------------------------------------- categories------------------------------------------------------------//
+ Future>> getCategories() async {
+ try {
+ final response = await _dio.get("api/explore/categories");
+
+ final List data = response.data['data'];
+ final categories = data.map((e) => ExploreCategory.fromMap(e)).toList();
+
+ return right(categories);
+ } catch (e) {
+ return left(AppFailure(message: "Failed to load categories"));
+ }
+ }
+
+ Future> saveUserInterests(
+ Set categories,
+ ) async {
+ try {
+ final response = await _dio.post(
+ "api/explore/preferred-categories",
+ data: {"categories": categories.toList()},
+ );
+
+ return right(response.data['message'] ?? "Interests saved");
+ } catch (e) {
+ return left(AppFailure(message: "Failed to save interests"));
+ }
+ }
+
+ Future>> getUserInterests() async {
+ try {
+ final response = await _dio.get("api/explore/preferred-categories");
+ final list = response.data['preferredCategories'] as List;
+ final names = list.map((e) => e['name'].toString()).toList();
+
+ return right(names);
+ } catch (e) {
+ return left(AppFailure(message: "Failed to load interests"));
+ }
+ }
+
//--------------------------------------------SignUp---------------------------------------------------------//
// Register new user
Future> create({
@@ -293,6 +343,7 @@ class AuthRemoteRepository {
);
final newUsername = response.data['user']['username'] as String;
final updatedUser = currentUser.copyWith(username: newUsername);
+ print("asermohamed after update username${response.data['tokens']}");
final newtokens = TokensModel.fromMap_update(response.data['tokens']);
return right((updatedUser, newtokens));
} on DioException {
diff --git a/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart b/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart
index 329eeed..094c0d5 100644
--- a/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/CreateAccount_Screen.dart
@@ -6,7 +6,7 @@ import 'package:lite_x/core/providers/dobProvider.dart';
import 'package:lite_x/core/providers/emailProvider.dart';
import 'package:lite_x/core/providers/nameProvider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -321,18 +321,24 @@ class _CreateAccountScreenState extends ConsumerState {
return Container(
padding: const EdgeInsets.all(10),
alignment: Alignment.centerRight,
- child: SizedBox(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 45),
child: ElevatedButton(
onPressed: (_isFormValid && !isLoading) ? _handleNext : null,
style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 50),
),
child: const Text(
'Next',
+ maxLines: 1,
+ softWrap: false,
style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
),
),
diff --git a/lib/features/auth/view/screens/Create_Account/Interests.dart b/lib/features/auth/view/screens/Create_Account/Interests.dart
index bd1b2d8..3f59145 100644
--- a/lib/features/auth/view/screens/Create_Account/Interests.dart
+++ b/lib/features/auth/view/screens/Create_Account/Interests.dart
@@ -7,6 +7,7 @@ import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/buildXLogo.dart';
import 'package:lite_x/features/auth/view_model/auth_state.dart';
import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
+import 'package:lite_x/features/auth/models/ExploreCategory.dart';
class Interests extends ConsumerStatefulWidget {
const Interests({super.key});
@@ -16,34 +17,59 @@ class Interests extends ConsumerStatefulWidget {
}
class _InterestsState extends ConsumerState {
- final Set _selectedInterests = {};
- final Map _availableInterests = {
- 'Sports': 'sports',
- 'Entertainment': 'entertainment',
- 'News': 'news',
- 'Technology': 'tech',
- 'Music': 'music',
- 'Gaming': 'gaming',
- 'Fashion & Beauty': 'fashion',
- 'Food': 'food',
- 'Business & Finance': 'business',
- 'Science': 'science',
- };
+ final Set _selectedCategoryNames = {};
+ List _categories = [];
+ bool _isLoadingCategories = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadCategories();
+ }
+
+ Future _loadCategories() async {
+ setState(() {
+ _isLoadingCategories = true;
+ });
+
+ final categories = await ref
+ .read(authViewModelProvider.notifier)
+ .getCategories();
+
+ setState(() {
+ _categories = categories;
+ _isLoadingCategories = false;
+ });
+
+ if (categories.isEmpty) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text(
+ 'Failed to load categories',
+ style: TextStyle(color: Colors.white),
+ ),
+ backgroundColor: Colors.black,
+ ),
+ );
+ }
+ }
+ }
void _handleNext() {
- if (_selectedInterests.isNotEmpty) {
+ if (_selectedCategoryNames.isNotEmpty) {
ref
.read(authViewModelProvider.notifier)
- .saveInterests(_selectedInterests);
+ .saveInterests(_selectedCategoryNames);
}
}
- void _toggleInterest(String interestId) {
+ void _toggleInterest(String categoryName) {
setState(() {
- if (_selectedInterests.contains(interestId)) {
- _selectedInterests.remove(interestId);
+ if (_selectedCategoryNames.contains(categoryName)) {
+ _selectedCategoryNames.remove(categoryName);
} else {
- _selectedInterests.add(interestId);
+ _selectedCategoryNames.add(categoryName);
}
});
}
@@ -57,17 +83,21 @@ class _InterestsState extends ConsumerState {
} else if (next.type == AuthStateType.error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text(next.message ?? 'Failed to save interests'),
- backgroundColor: Palette.textWhite,
+ content: Text(
+ next.message ?? 'Failed to save interests',
+ style: TextStyle(color: Palette.textWhite),
+ ),
+ backgroundColor: Palette.background,
),
);
- ref.read(authViewModelProvider.notifier).setAuthenticated();
+ ref.read(authViewModelProvider.notifier).setAuthenticated(); //
}
});
final authState = ref.watch(authViewModelProvider);
final isLoading = authState.isLoading;
- final bool isNextEnabled = _selectedInterests.isNotEmpty;
+ final bool isNextEnabled = _selectedCategoryNames.isNotEmpty;
+
return Scaffold(
backgroundColor: Palette.background,
appBar: AppBar(
@@ -81,34 +111,55 @@ class _InterestsState extends ConsumerState {
absorbing: isLoading,
child: Stack(
children: [
- SingleChildScrollView(
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- 'What do you want to see on X ?',
- style: TextStyle(
- fontSize: 22,
- fontWeight: FontWeight.w800,
- color: Palette.textWhite,
+ if (_isLoadingCategories)
+ const Center(child: Loader())
+ else
+ SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 16,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'What do you want to see on X ?',
+ style: TextStyle(
+ fontSize: 22,
+ fontWeight: FontWeight.w800,
+ color: Palette.textWhite,
+ ),
),
- ),
- const SizedBox(height: 10),
- const Text(
- 'Select topics you\'re interested in to help personalize your experience. You can change these any time.',
- style: TextStyle(
- fontSize: 16,
- color: Palette.textSecondary,
+ const SizedBox(height: 10),
+ const Text(
+ 'Select topics you\'re interested in to help personalize your experience. You can change these any time.',
+ style: TextStyle(
+ fontSize: 16,
+ color: Palette.textSecondary,
+ ),
),
- ),
- const SizedBox(height: 28),
- _buildInterestsWrap(),
- const SizedBox(height: 125),
- ],
+ const SizedBox(height: 28),
+ if (_categories.isEmpty)
+ const Center(
+ child: Padding(
+ padding: EdgeInsets.all(20.0),
+ child: Text(
+ 'No categories available',
+ style: TextStyle(
+ color: Palette.textTertiary,
+ fontSize: 16,
+ ),
+ ),
+ ),
+ )
+ else
+ _buildInterestsWrap(),
+ const SizedBox(height: 125),
+ ],
+ ),
),
- ),
- _buildNextButton(isNextEnabled, isLoading),
+ if (!_isLoadingCategories)
+ _buildNextButton(isNextEnabled, isLoading),
if (isLoading)
Container(color: Colors.black, child: const Loader()),
],
@@ -118,29 +169,42 @@ class _InterestsState extends ConsumerState {
}
Widget _buildInterestsWrap() {
- return Wrap(
- spacing: 12.0,
- runSpacing: 12.0,
- children: _availableInterests.entries.map((entry) {
- final String label = entry.key;
- final String id = entry.value;
- final bool isSelected = _selectedInterests.contains(id);
-
+ return GridView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ padding: EdgeInsets.zero,
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 2,
+ childAspectRatio: 5,
+ crossAxisSpacing: 10.0,
+ mainAxisSpacing: 10.0,
+ ),
+ itemCount: _categories.length,
+ itemBuilder: (context, index) {
+ final category = _categories[index];
+ final bool isSelected = _selectedCategoryNames.contains(category.name);
return FilterChip(
- label: Text(
- label,
- style: TextStyle(
- color: isSelected ? Palette.background : Palette.textWhite,
- fontWeight: FontWeight.bold,
+ label: SizedBox(
+ width: double.infinity,
+ child: Text(
+ category.name,
+ textAlign: TextAlign.center,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ color: isSelected ? Palette.background : Palette.textWhite,
+ fontWeight: FontWeight.bold,
+ fontSize: 12,
+ ),
),
),
selected: isSelected,
onSelected: (bool selected) {
- _toggleInterest(id);
+ _toggleInterest(category.name);
},
backgroundColor: Palette.cardBackground,
selectedColor: Palette.primary,
checkmarkColor: Palette.background,
+ showCheckmark: false,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
side: BorderSide(
@@ -148,9 +212,9 @@ class _InterestsState extends ConsumerState {
width: 1.5,
),
),
- padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
+ padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 0),
);
- }).toList(),
+ },
);
}
@@ -158,29 +222,38 @@ class _InterestsState extends ConsumerState {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
- padding: const EdgeInsets.only(
- bottom: 20,
- left: 20,
- right: 20,
- top: 20,
- ),
- decoration: BoxDecoration(color: Palette.background),
width: double.infinity,
- child: ElevatedButton(
- onPressed: (isEnabled && !isLoading) ? _handleNext : null,
- style: ElevatedButton.styleFrom(
- backgroundColor: Palette.textWhite,
- disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
- foregroundColor: Palette.background,
- disabledForegroundColor: Palette.textSecondary,
- minimumSize: const Size(double.infinity, 50),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
+ padding: const EdgeInsets.fromLTRB(16, 10, 16, 30),
+ color: Palette.background,
+ child: SizedBox(
+ width: double.infinity,
+ height: 50,
+ child: ElevatedButton(
+ onPressed: (isEnabled && !isLoading) ? _handleNext : null,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Palette.textWhite,
+ disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
+ foregroundColor: Palette.background,
+ disabledForegroundColor: Palette.textSecondary,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
),
- ),
- child: const Text(
- 'Next',
- style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
+ child: isLoading
+ ? const SizedBox(
+ width: 24,
+ height: 24,
+ child: CircularProgressIndicator(
+ strokeWidth: 2.5,
+ valueColor: AlwaysStoppedAnimation(
+ Palette.background,
+ ),
+ ),
+ )
+ : const Text(
+ 'Next',
+ style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
+ ),
),
),
),
diff --git a/lib/features/auth/view/screens/Create_Account/Password_Screen.dart b/lib/features/auth/view/screens/Create_Account/Password_Screen.dart
index 0c785d9..78adaa0 100644
--- a/lib/features/auth/view/screens/Create_Account/Password_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/Password_Screen.dart
@@ -1,11 +1,9 @@
-// ignore_for_file: unused_field
-
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/emailProvider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -27,23 +25,16 @@ class _PasswordScreenState extends ConsumerState {
final _passFocus = FocusNode();
final _isFormValid = ValueNotifier(false);
- bool _isPassFocused = false;
-
@override
void initState() {
super.initState();
_passwordController.addListener(_validateForm);
- _passFocus.addListener(() {
- setState(() {
- _isPassFocused = _passFocus.hasFocus;
- });
- });
}
void _validateForm() {
final passwordValid =
_passwordController.text.trim().isNotEmpty &&
- _passwordController.text.length >= 8;
+ _passwordController.text.length >= 9;
_isFormValid.value = passwordValid;
}
@@ -88,82 +79,85 @@ class _PasswordScreenState extends ConsumerState {
return Scaffold(
backgroundColor: Palette.background,
+ resizeToAvoidBottomInset: false,
appBar: AppBar(
title: buildXLogo(size: 36),
centerTitle: true,
backgroundColor: Palette.background,
elevation: 0,
),
- body: AbsorbPointer(
- absorbing: isLoading,
- child: Stack(
- children: [
- Center(
- child: Container(
- width: double.infinity,
- height: double.infinity,
- decoration: BoxDecoration(color: Palette.background),
- child: Column(
- children: [
- Expanded(
- child: Form(
- key: _formKey,
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 16,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- 'You\'ll need a password',
- style: TextStyle(
- fontSize: 26,
- fontWeight: FontWeight.w800,
- color: Palette.textWhite,
+ body: SafeArea(
+ child: AbsorbPointer(
+ absorbing: isLoading,
+ child: Stack(
+ children: [
+ Center(
+ child: Container(
+ width: double.infinity,
+ height: double.infinity,
+ decoration: BoxDecoration(color: Palette.background),
+ child: Column(
+ children: [
+ Expanded(
+ child: Form(
+ key: _formKey,
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 32,
+ vertical: 16,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ 'You\'ll need a password',
+ style: TextStyle(
+ fontSize: 26,
+ fontWeight: FontWeight.w800,
+ color: Palette.textWhite,
+ ),
+ ),
+ const SizedBox(height: 10),
+ const Text(
+ 'Make sure it\'s 9 characters or more.',
+ style: TextStyle(
+ fontSize: 14,
+ color: Palette.greycolor,
+ ),
),
- ),
- const SizedBox(height: 10),
- const Text(
- 'Make sure it\'s 8 characters or more.',
- style: TextStyle(
- fontSize: 14,
- color: Palette.greycolor,
+ const SizedBox(height: 16),
+ CustomTextField(
+ controller: _passwordController,
+ focusNode: _passFocus,
+ labelText: 'Password',
+ isPassword: true,
+ validator: passwordValidator,
+ onFieldSubmitted: (_) {
+ if (_isFormValid.value) {
+ _handleSignUp();
+ }
+ },
),
- ),
- const SizedBox(height: 16),
- CustomTextField(
- controller: _passwordController,
- focusNode: _passFocus,
- labelText: 'Password',
- isPassword: true,
- validator: passwordValidator,
- onFieldSubmitted: (_) {
- if (_isFormValid.value) {
- _handleSignUp();
- }
- },
- ),
- const SizedBox(height: 70),
- buildTermsTextP(),
- ],
+ const SizedBox(height: 70),
+ buildTermsTextP(),
+ ],
+ ),
),
),
),
- ),
- _buildSignUpButton(isLoading),
- const SizedBox(height: 15),
- ],
+ _buildSignUpButton(isLoading),
+ const SizedBox(height: 15),
+ ],
+ ),
),
),
- ),
- if (isLoading)
- Container(
- color: Colors.black.withOpacity(0.5),
- child: const Center(child: Loader()),
- ),
- ],
+ if (isLoading)
+ Container(
+ color: Colors.black.withOpacity(0.5),
+ child: const Center(child: Loader()),
+ ),
+ ],
+ ),
),
),
);
@@ -176,16 +170,19 @@ class _PasswordScreenState extends ConsumerState {
child: ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- // width: 120,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 100, minHeight: 45),
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleSignUp : null,
style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 6,
+ ),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 50),
),
child: isLoading
? const SizedBox(
@@ -200,6 +197,8 @@ class _PasswordScreenState extends ConsumerState {
)
: const Text(
'Sign up',
+ maxLines: 1,
+ softWrap: false,
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.bold,
diff --git a/lib/features/auth/view/screens/Create_Account/Upload_Profile_Photo_Screen.dart b/lib/features/auth/view/screens/Create_Account/Upload_Profile_Photo_Screen.dart
index aff1c73..12c222b 100644
--- a/lib/features/auth/view/screens/Create_Account/Upload_Profile_Photo_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/Upload_Profile_Photo_Screen.dart
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lite_x/core/classes/PickedImage.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/buildXLogo.dart';
import 'package:dotted_border/dotted_border.dart';
@@ -83,7 +83,6 @@ class _UploadProfilePhotoScreenState
Future _handleNext() async {
if (selectedImage != null) {
- // Upload the profile photo
await ref
.read(authViewModelProvider.notifier)
.uploadProfilePhoto(selectedImage!);
@@ -257,50 +256,59 @@ class _UploadProfilePhotoScreenState
}
Widget _buildBottomButtons() {
- return Container(
- padding: const EdgeInsets.all(16),
+ return Padding(
+ padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- OutlinedButton(
- onPressed: _handleSkip,
- style: OutlinedButton.styleFrom(
- foregroundColor: Palette.textWhite,
- side: const BorderSide(color: Palette.textWhite, width: 1),
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 40, minWidth: 90),
+ child: OutlinedButton(
+ onPressed: _handleSkip,
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Palette.textWhite,
+ side: const BorderSide(color: Palette.textWhite, width: 1),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 10,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ ),
+ child: const Text(
+ 'Skip for now',
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
- ),
- child: const Text(
- 'Skip for now',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 90,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 40),
child: ElevatedButton(
onPressed: isValid ? _handleNext : null,
style: ElevatedButton.styleFrom(
- padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 5,
- ),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 10,
+ ),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
'Next',
- style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
);
diff --git a/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart b/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart
index eb091d7..eaadee4 100644
--- a/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/UserName_Screen.dart
@@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -276,36 +276,45 @@ class _UsernameScreenState extends ConsumerState {
Widget _buildBottomButtons() {
return Container(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- OutlinedButton(
- onPressed: _handleSkip,
- style: OutlinedButton.styleFrom(
- foregroundColor: Palette.textWhite,
- side: const BorderSide(color: Palette.textWhite, width: 1),
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 45, minWidth: 80),
+ child: OutlinedButton(
+ onPressed: _handleSkip,
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Palette.textWhite,
+ side: const BorderSide(color: Palette.textWhite, width: 1),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 10,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ ),
+ child: const Text(
+ 'Skip for now',
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
- ),
- child: const Text(
- 'Skip for now',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
+
ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 90,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 45),
child: ElevatedButton(
onPressed: isValid ? _handleNext : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 5,
+ horizontal: 20,
+ vertical: 10,
),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
@@ -317,7 +326,9 @@ class _UsernameScreenState extends ConsumerState {
),
child: const Text(
'Next',
- style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
);
diff --git a/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart b/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart
index 27970df..75d81ed 100644
--- a/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart
+++ b/lib/features/auth/view/screens/Create_Account/Verification_Screen.dart
@@ -5,7 +5,7 @@ import 'package:lite_x/core/providers/dobProvider.dart';
import 'package:lite_x/core/providers/emailProvider.dart';
import 'package:lite_x/core/providers/nameProvider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -191,25 +191,29 @@ class _VerificationScreenState extends ConsumerState {
Widget _buildNextButton(bool isLoading) {
return Container(
padding: EdgeInsets.all(10),
-
alignment: Alignment.centerRight,
child: ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 90,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 45),
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleNext : null,
style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 8,
+ ),
backgroundColor: Palette.textWhite,
- disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
+ disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 40),
),
child: const Text(
'Next',
- style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
+ softWrap: false,
+ overflow: TextOverflow.clip,
+ style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
),
);
diff --git a/lib/features/auth/view/screens/Intro_Screen.dart b/lib/features/auth/view/screens/Intro_Screen.dart
index cdf523a..0b4b736 100644
--- a/lib/features/auth/view/screens/Intro_Screen.dart
+++ b/lib/features/auth/view/screens/Intro_Screen.dart
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/buildTermsText.dart';
import 'package:lite_x/features/auth/view/widgets/buildXLogo.dart';
@@ -41,7 +41,16 @@ class IntroScreen extends ConsumerWidget {
final authViewModel = ref.read(authViewModelProvider.notifier);
if (next.type == AuthStateType.authenticated) {
- context.goNamed(RouteConstants.setbirthdate);
+ if (next.message == "new_google_user" ||
+ next.message == "new_github_user") {
+ context.goNamed(RouteConstants.setbirthdate);
+ return;
+ }
+ if (next.message == "google_login_success" ||
+ next.message == "github_login_success") {
+ context.goNamed(RouteConstants.homescreen);
+ return;
+ }
} else if (next.type == AuthStateType.error) {
_showErrorToast(
context,
@@ -78,36 +87,36 @@ class IntroScreen extends ConsumerWidget {
}
Widget _buildMobileLayout(Size size, BuildContext context, WidgetRef ref) {
- return SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(height: 2),
- Center(child: buildXLogo(size: 50)),
- SizedBox(height: size.height * 0.15),
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 5.0),
- child: Text(
- 'See what\'s\nhappening in the\nworld right now.',
- textAlign: TextAlign.start,
- style: TextStyle(
- fontSize: 34,
- fontWeight: FontWeight.w700,
- color: Colors.white,
- ),
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ mainAxisSize: MainAxisSize.max,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 10),
+ Center(child: buildXLogo(size: 46)),
+ // SizedBox(height: size.height * 0.15),
+ const Spacer(flex: 1),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 5.0),
+ child: Text(
+ 'See what\'s\nhappening in the\nworld right now.',
+ textAlign: TextAlign.start,
+ style: TextStyle(
+ fontSize: 34,
+ fontWeight: FontWeight.w700,
+ color: Colors.white,
),
),
- SizedBox(height: size.height * 0.15),
- _buildAuthButtons(context, ref),
- const SizedBox(height: 20),
- buildTermsText(),
- const SizedBox(height: 5),
- _buildLoginSection(context),
- ],
- ),
+ ),
+ const Spacer(flex: 1),
+ _buildAuthButtons(context, ref),
+ const SizedBox(height: 20),
+ buildTermsText(),
+ const SizedBox(height: 5),
+ _buildLoginSection(context),
+ const SizedBox(height: 30),
+ ],
),
);
}
@@ -171,7 +180,7 @@ class IntroScreen extends ConsumerWidget {
children: [
const Text(
'Have an account already? ',
- style: TextStyle(color: Colors.grey, fontSize: 14),
+ style: TextStyle(color: Colors.grey, fontSize: 16),
),
GestureDetector(
onTap: () {
@@ -181,7 +190,7 @@ class IntroScreen extends ConsumerWidget {
'Log in',
style: TextStyle(
color: Palette.info,
- fontSize: 14,
+ fontSize: 16,
fontWeight: FontWeight.w600,
),
),
diff --git a/lib/features/auth/view/screens/Log_In/Change_Password_Feedback.dart b/lib/features/auth/view/screens/Log_In/Change_Password_Feedback.dart
index 9e1aad6..ccf3144 100644
--- a/lib/features/auth/view/screens/Log_In/Change_Password_Feedback.dart
+++ b/lib/features/auth/view/screens/Log_In/Change_Password_Feedback.dart
@@ -4,7 +4,6 @@ import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/auth/view/widgets/buildXLogo.dart';
-import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
class ChangePasswordFeedback extends ConsumerStatefulWidget {
const ChangePasswordFeedback({super.key});
@@ -20,9 +19,9 @@ class _ChangePasswordFeedbackState
@override
void initState() {
super.initState();
- Future.microtask(() {
- ref.read(authViewModelProvider.notifier).resetState();
- });
+ // Future.microtask(() {
+ // ref.read(authViewModelProvider.notifier).resetState();//
+ // });
}
void _handleNext() {
@@ -170,23 +169,26 @@ class _ChangePasswordFeedbackState
padding: EdgeInsets.all(10),
alignment: Alignment.centerRight,
child: SizedBox(
- width: 80,
- child: ElevatedButton(
- onPressed: _selectedMethod.isNotEmpty ? _handleNext : null,
- style: ElevatedButton.styleFrom(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
- backgroundColor: Palette.textWhite,
- disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
- foregroundColor: Palette.background,
- disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 30),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
+ width: 100,
+ height: 45,
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: ElevatedButton(
+ onPressed: _selectedMethod.isNotEmpty ? _handleNext : null,
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
+ backgroundColor: Palette.textWhite,
+ disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
+ foregroundColor: Palette.background,
+ disabledForegroundColor: Palette.border,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ ),
+ child: const Text(
+ 'Next',
+ style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
- ),
- child: const Text(
- 'Next',
- style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
),
),
diff --git a/lib/features/auth/view/screens/Log_In/Choose_New_Password_Screen.dart b/lib/features/auth/view/screens/Log_In/Choose_New_Password_Screen.dart
index 6ca816c..5e92f30 100644
--- a/lib/features/auth/view/screens/Log_In/Choose_New_Password_Screen.dart
+++ b/lib/features/auth/view/screens/Log_In/Choose_New_Password_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/emailProvider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -78,8 +78,8 @@ class _ChooseNewPasswordScreenState
if (value == null || value.isEmpty) {
return 'Password is required';
}
- if (value.length < 8) {
- return 'Password must be at least 8 characters';
+ if (value.length < 9) {
+ return 'Password must be at least 9 characters';
}
if (value != _newpasswordController.text) {
return 'Passwords do not match';
@@ -307,20 +307,21 @@ class _ChooseNewPasswordScreenState
child: ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 200,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 140, minHeight: 45),
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleChangePassword : null,
style: ElevatedButton.styleFrom(
backgroundColor: Palette.textWhite,
- disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
+ disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 40),
),
child: const Text(
'Change password',
- style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
),
);
diff --git a/lib/features/auth/view/screens/Log_In/Confirmation_code_Loc_Screen.dart b/lib/features/auth/view/screens/Log_In/Confirmation_code_Loc_Screen.dart
index 83e9b02..75fb8f1 100644
--- a/lib/features/auth/view/screens/Log_In/Confirmation_code_Loc_Screen.dart
+++ b/lib/features/auth/view/screens/Log_In/Confirmation_code_Loc_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/emailProvider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/buildXLogo.dart';
import 'package:lite_x/features/auth/view_model/auth_state.dart';
@@ -248,46 +248,59 @@ class _ConfirmationCodeLocScreenState
Widget _buildBottomButtons(bool isLoading) {
return Container(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- OutlinedButton(
- onPressed: isLoading ? null : _handleCancel,
- style: OutlinedButton.styleFrom(
- foregroundColor: Palette.textWhite,
- side: const BorderSide(color: Palette.textWhite, width: 1),
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
- ),
- ),
- child: const Text(
- 'Cancel',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
- ),
- ),
- SizedBox(
- width: 80,
- child: ElevatedButton(
- onPressed: isLoading ? null : _handleNext,
- style: ElevatedButton.styleFrom(
+ ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 45, minWidth: 80),
+ child: OutlinedButton(
+ onPressed: isLoading ? null : _handleCancel,
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Palette.textWhite,
+ side: const BorderSide(color: Palette.textWhite, width: 1),
padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 5,
+ horizontal: 20,
+ vertical: 10,
),
- backgroundColor: Palette.textWhite,
- disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
- foregroundColor: Palette.background,
- disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
- 'Next',
- style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
+ 'Cancel',
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
+ ),
+ ),
+ ),
+ SizedBox(
+ width: 100,
+ height: 45,
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: ElevatedButton(
+ onPressed: isLoading ? null : _handleNext,
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 10,
+ ),
+ backgroundColor: Palette.textWhite,
+ disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
+ foregroundColor: Palette.background,
+ disabledForegroundColor: Palette.border,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ ),
+ child: const Text(
+ 'Next',
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ ),
),
),
),
diff --git a/lib/features/auth/view/screens/Log_In/ForgotPassword_Screen.dart b/lib/features/auth/view/screens/Log_In/ForgotPassword_Screen.dart
index f965d6f..44910f5 100644
--- a/lib/features/auth/view/screens/Log_In/ForgotPassword_Screen.dart
+++ b/lib/features/auth/view/screens/Log_In/ForgotPassword_Screen.dart
@@ -144,7 +144,7 @@ class _ForgotpasswordScreenState extends ConsumerState {
),
const SizedBox(height: 12),
const Text(
- 'Enter the email, phone number or username associated with your account to change your password.',
+ 'Enter the email associated with your account to change your password.',
style: TextStyle(
fontSize: 15,
color: Palette.textSecondary,
@@ -153,8 +153,7 @@ class _ForgotpasswordScreenState extends ConsumerState {
const SizedBox(height: 10),
CustomTextField(
controller: _identifiercontroller,
- labelText:
- 'Email address, phone number or username',
+ labelText: 'Email address',
keyboardType: TextInputType.emailAddress,
validator: emailValidator,
onFieldSubmitted: (_) {
@@ -188,23 +187,27 @@ class _ForgotpasswordScreenState extends ConsumerState {
child: ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 80,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 45),
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleNext : null,
style: ElevatedButton.styleFrom(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 10,
+ ),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
'Next',
+ maxLines: 1,
+ softWrap: false,
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
),
diff --git a/lib/features/auth/view/screens/Log_In/LoginPasswordScreen.dart b/lib/features/auth/view/screens/Log_In/LoginPasswordScreen.dart
index 0971bd6..230f002 100644
--- a/lib/features/auth/view/screens/Log_In/LoginPasswordScreen.dart
+++ b/lib/features/auth/view/screens/Log_In/LoginPasswordScreen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/emailProvider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -100,7 +100,6 @@ class _LoginPasswordScreenState extends ConsumerState {
if (next.type == AuthStateType.authenticated) {
context.goNamed(RouteConstants.homescreen);
- authViewModel.resetState();
} else if (next.type == AuthStateType.error) {
_showErrorToast(next.message ?? 'Login failed. Please try again.');
authViewModel.resetState();
@@ -193,48 +192,57 @@ class _LoginPasswordScreenState extends ConsumerState {
Widget _buildBottomButtons(bool isLoading) {
return Container(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- OutlinedButton(
- onPressed: isLoading ? null : _handleForgotPassword,
- style: OutlinedButton.styleFrom(
- foregroundColor: Palette.textWhite,
- side: const BorderSide(color: Palette.textWhite, width: 1),
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 45),
+ child: OutlinedButton(
+ onPressed: isLoading ? null : _handleForgotPassword,
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Palette.textWhite,
+ side: const BorderSide(color: Palette.textWhite, width: 1),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 10,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ ),
+ child: const Text(
+ 'Forgot password?',
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
- ),
- child: const Text(
- 'Forgot password?',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 80,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 45),
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleLogin : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 5,
+ horizontal: 20,
+ vertical: 10,
),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
'Log in',
+ maxLines: 1,
+ softWrap: false,
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
),
diff --git a/lib/features/auth/view/screens/Log_In/Login_Screen.dart b/lib/features/auth/view/screens/Log_In/Login_Screen.dart
index fed9a5e..0c62509 100644
--- a/lib/features/auth/view/screens/Log_In/Login_Screen.dart
+++ b/lib/features/auth/view/screens/Log_In/Login_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/emailProvider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -133,7 +133,7 @@ class _LoginScreenState extends ConsumerState {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
- 'To get started, first enter your phone, email address or @username',
+ 'To get started, first enter your Email address',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w800,
@@ -143,7 +143,7 @@ class _LoginScreenState extends ConsumerState {
const SizedBox(height: 20),
CustomTextField(
controller: _identifiercontroller,
- labelText: 'Phone, email address, or username',
+ labelText: 'Email address',
keyboardType: TextInputType.emailAddress,
validator: emailValidator,
onFieldSubmitted: (_) {
@@ -172,47 +172,57 @@ class _LoginScreenState extends ConsumerState {
Widget _buildBottomButtons() {
return Container(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- OutlinedButton(
- onPressed: _handleForgotPassword,
- style: OutlinedButton.styleFrom(
- foregroundColor: Palette.textWhite,
- side: const BorderSide(color: Palette.textWhite, width: 1),
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 45),
+ child: OutlinedButton(
+ onPressed: _handleForgotPassword,
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Palette.textWhite,
+ side: const BorderSide(color: Palette.textWhite, width: 1),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 18,
+ vertical: 10,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ ),
+ child: const Text(
+ 'Forgot password?',
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
- ),
- child: const Text(
- 'Forgot password?',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
+
ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 80,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 45),
child: ElevatedButton(
onPressed: isValid ? _handleNext : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 5,
+ horizontal: 20,
+ vertical: 10,
),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
+ maxLines: 1,
+ softWrap: false,
'Next',
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
diff --git a/lib/features/auth/view/screens/Log_In/VerificationForgot_Screen.dart b/lib/features/auth/view/screens/Log_In/VerificationForgot_Screen.dart
index 4b330ef..48e9c67 100644
--- a/lib/features/auth/view/screens/Log_In/VerificationForgot_Screen.dart
+++ b/lib/features/auth/view/screens/Log_In/VerificationForgot_Screen.dart
@@ -153,7 +153,7 @@ class _VerificationforgotScreenState
validator: verificationCodeValidator,
controller: _codeController,
labelText: 'Enter your code',
- keyboardType: TextInputType.text,
+ keyboardType: TextInputType.number,
onFieldSubmitted: (_) {
if (_isFormValid.value && !isLoading) {
_handleNext();
@@ -180,48 +180,57 @@ class _VerificationforgotScreenState
Widget _buildBottomButtons(bool isLoading) {
return Container(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- OutlinedButton(
- onPressed: isLoading ? null : () => context.pop(),
- style: OutlinedButton.styleFrom(
- foregroundColor: Palette.textWhite,
- side: const BorderSide(color: Palette.textWhite, width: 1),
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 6),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 45, minWidth: 80),
+ child: OutlinedButton(
+ onPressed: isLoading ? null : () => context.pop(),
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Palette.textWhite,
+ side: const BorderSide(color: Palette.textWhite, width: 1),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 10,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(25),
+ ),
+ ),
+ child: const Text(
+ 'Back',
+ maxLines: 1,
+ softWrap: false,
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
- ),
- child: const Text(
- 'Back',
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 80,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 90, minHeight: 40),
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleNext : null,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 5,
+ horizontal: 20,
+ vertical: 8,
),
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.6),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
child: const Text(
'Next',
+ maxLines: 1,
+ softWrap: false,
style: TextStyle(fontSize: 19, fontWeight: FontWeight.bold),
),
),
diff --git a/lib/features/auth/view/screens/Oauth/SetBirthdate.dart b/lib/features/auth/view/screens/Oauth/SetBirthdate.dart
index 446b3e8..15ebc17 100644
--- a/lib/features/auth/view/screens/Oauth/SetBirthdate.dart
+++ b/lib/features/auth/view/screens/Oauth/SetBirthdate.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/core/utils.dart';
import 'package:lite_x/core/view/widgets/Loader.dart';
import 'package:lite_x/features/auth/view/widgets/CustomTextField.dart';
@@ -189,16 +189,23 @@ class _SetbirthdateState extends ConsumerState {
child: ValueListenableBuilder(
valueListenable: _isFormValid,
builder: (context, isValid, child) {
- return SizedBox(
- width: 120,
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 100, minHeight: 45),
child: ElevatedButton(
onPressed: (isValid && !isLoading) ? _handleSignUp : null,
style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 20,
+ vertical: 8,
+ ),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(30),
+ ),
+
backgroundColor: Palette.textWhite,
disabledBackgroundColor: Palette.textWhite.withOpacity(0.5),
foregroundColor: Palette.background,
disabledForegroundColor: Palette.border,
- minimumSize: const Size(0, 38),
),
child: isLoading
? const SizedBox(
@@ -213,6 +220,8 @@ class _SetbirthdateState extends ConsumerState {
)
: const Text(
'Sign up',
+ maxLines: 1,
+ softWrap: false,
style: TextStyle(
fontSize: 19,
fontWeight: FontWeight.bold,
diff --git a/lib/features/auth/view/widgets/CustomTextField.dart b/lib/features/auth/view/widgets/CustomTextField.dart
index 25e6a39..0c5f9b5 100644
--- a/lib/features/auth/view/widgets/CustomTextField.dart
+++ b/lib/features/auth/view/widgets/CustomTextField.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
OutlineInputBorder border_sign(Color co, double w) => OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
diff --git a/lib/features/auth/view/widgets/buildTermsText.dart b/lib/features/auth/view/widgets/buildTermsText.dart
index 1a16316..fb895f4 100644
--- a/lib/features/auth/view/widgets/buildTermsText.dart
+++ b/lib/features/auth/view/widgets/buildTermsText.dart
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
Widget buildTermsText() {
return Text.rich(
TextSpan(
text: 'By signing up, you agree to our ',
- style: const TextStyle(color: Colors.grey, fontSize: 16),
+ style: const TextStyle(color: Colors.grey, fontSize: 14),
children: [
TextSpan(
text: 'Terms',
diff --git a/lib/features/auth/view/widgets/buildTermsTextP.dart b/lib/features/auth/view/widgets/buildTermsTextP.dart
index 200c97b..a5c6e50 100644
--- a/lib/features/auth/view/widgets/buildTermsTextP.dart
+++ b/lib/features/auth/view/widgets/buildTermsTextP.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
Widget buildTermsTextP() {
return RichText(
diff --git a/lib/features/auth/view/widgets/buildXLogo.dart b/lib/features/auth/view/widgets/buildXLogo.dart
index 70364a1..5ef370c 100644
--- a/lib/features/auth/view/widgets/buildXLogo.dart
+++ b/lib/features/auth/view/widgets/buildXLogo.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
Widget buildXLogo({required double size}) {
return SvgPicture.asset(
diff --git a/lib/features/auth/view_model/auth_view_model.dart b/lib/features/auth/view_model/auth_view_model.dart
index 8c49aa4..cd09806 100644
--- a/lib/features/auth/view_model/auth_view_model.dart
+++ b/lib/features/auth/view_model/auth_view_model.dart
@@ -1,13 +1,14 @@
import 'dart:io';
-
import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:flutter/material.dart';
import 'package:lite_x/core/classes/PickedImage.dart';
import 'package:lite_x/core/models/usermodel.dart';
+import 'package:lite_x/features/auth/models/ExploreCategory.dart';
import 'package:lite_x/features/auth/repositories/auth_local_repository.dart';
import 'package:lite_x/features/auth/repositories/auth_remote_repository.dart';
import 'package:lite_x/features/auth/view_model/auth_state.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
-import 'package:lite_x/features/chat/repositories/chat_local_repository.dart';
+// import 'package:lite_x/features/chat/repositories/chat_local_repository.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth_view_model.g.dart';
@@ -15,13 +16,13 @@ part 'auth_view_model.g.dart';
class AuthViewModel extends _$AuthViewModel {
late AuthRemoteRepository _authRemoteRepository;
late AuthLocalRepository _authLocalRepository;
- late ChatLocalRepository _chatLocalRepository;
+ // late ChatLocalRepository _chatLocalRepository;
@override
AuthState build() {
_authRemoteRepository = ref.read(authRemoteRepositoryProvider);
_authLocalRepository = ref.read(authLocalRepositoryProvider);
- _chatLocalRepository = ref.read(chatLocalRepositoryProvider);
+ // _chatLocalRepository = ref.read(chatLocalRepositoryProvider);
Future(() async {
await Future.delayed(const Duration(milliseconds: 300));
await _checkAuthStatus();
@@ -149,7 +150,17 @@ class AuthViewModel extends _$AuthViewModel {
);
}
- Future saveInterests(Set interests) async {
+ //--------------------------------------------Get Categories---------------------------------------------------------//
+ Future> getCategories() async {
+ final result = await _authRemoteRepository.getCategories();
+ return result.fold((failure) {
+ print("Failed to load categories: ${failure.message}");
+ return [];
+ }, (categories) => categories);
+ }
+
+ //--------------------------------------------Save Interests---------------------------------------------------------//
+ Future saveInterests(Set categoriesnames) async {
state = AuthState.loading();
try {
final currentUser = ref.read(currentUserProvider);
@@ -157,10 +168,22 @@ class AuthViewModel extends _$AuthViewModel {
state = AuthState.error("User not found!");
return;
}
- final updatedUser = currentUser.copyWith(interests: interests);
- await _authLocalRepository.saveUser(updatedUser);
- ref.read(currentUserProvider.notifier).adduser(updatedUser);
- state = AuthState.success("Interests saved successfully");
+
+ final result = await _authRemoteRepository.saveUserInterests(
+ categoriesnames,
+ );
+
+ await result.fold(
+ (failure) async {
+ state = AuthState.error(failure.message);
+ },
+ (message) async {
+ final updatedUser = currentUser.copyWith(interests: categoriesnames);
+ await _authLocalRepository.saveUser(updatedUser);
+ ref.read(currentUserProvider.notifier).adduser(updatedUser);
+ state = AuthState.success(message);
+ },
+ );
} catch (e) {
state = AuthState.error(e.toString());
}
@@ -207,6 +230,7 @@ class AuthViewModel extends _$AuthViewModel {
//-------------------------------------------------Login--------------------------------------------------------------------------------------//
Future login({required String email, required String password}) async {
state = AuthState.loading();
+
final result = await _authRemoteRepository.login(
email: email,
password: password,
@@ -221,6 +245,20 @@ class AuthViewModel extends _$AuthViewModel {
_authLocalRepository.saveTokens(tokens),
]);
ref.read(currentUserProvider.notifier).adduser(user);
+ final interestsResult = await _authRemoteRepository.getUserInterests();
+
+ interestsResult.fold(
+ (err) {
+ print("Failed to load interests");
+ },
+ (interestsList) async {
+ final interestsSet = interestsList.toSet();
+ final updatedUser = user.copyWith(interests: interestsSet);
+ ref.read(currentUserProvider.notifier).adduser(updatedUser);
+ await _authLocalRepository.saveUser(updatedUser);
+ },
+ );
+
state = AuthState.authenticated('Login successful');
if (!Platform.environment.containsKey('FLUTTER_TEST')) {
_registerFcmToken();
@@ -234,7 +272,6 @@ class AuthViewModel extends _$AuthViewModel {
try {
await _authLocalRepository.clearTokens();
await _authLocalRepository.clearUser();
- await _chatLocalRepository.clearAll();
ref.read(currentUserProvider.notifier).clearUser();
state = AuthState.unauthenticated();
} catch (e) {
@@ -530,13 +567,33 @@ class AuthViewModel extends _$AuthViewModel {
state = AuthState.error(failure.message);
},
(data) async {
- final (user, tokens) = data;
+ final (user, tokens, newuser) = data;
await Future.wait([
_authLocalRepository.saveUser(user),
_authLocalRepository.saveTokens(tokens),
]);
ref.read(currentUserProvider.notifier).adduser(user);
- state = AuthState.authenticated('Signup successful');
+
+ if (newuser) {
+ state = AuthState.authenticated("new_google_user");
+ _registerFcmToken();
+ _listenForFcmTokenRefresh();
+ return;
+ }
+
+ final interestsResult = await _authRemoteRepository.getUserInterests();
+ await interestsResult.fold(
+ (err) async {
+ debugPrint("Failed to fetch interests");
+ },
+ (list) async {
+ final updated = user.copyWith(interests: list.toSet());
+ ref.read(currentUserProvider.notifier).adduser(updated);
+ await _authLocalRepository.saveUser(updated);
+ },
+ );
+
+ state = AuthState.authenticated("google_login_success");
_registerFcmToken();
_listenForFcmTokenRefresh();
},
@@ -551,14 +608,32 @@ class AuthViewModel extends _$AuthViewModel {
state = AuthState.error(failure.message);
},
(data) async {
- final (user, tokens) = data;
+ final (user, tokens, newuser) = data;
await Future.wait([
_authLocalRepository.saveUser(user),
_authLocalRepository.saveTokens(tokens),
]);
-
ref.read(currentUserProvider.notifier).adduser(user);
- state = AuthState.authenticated('Social login successful');
+ if (newuser) {
+ state = AuthState.authenticated("new_github_user");
+ _registerFcmToken();
+ _listenForFcmTokenRefresh();
+ return;
+ }
+
+ final interestsResult = await _authRemoteRepository.getUserInterests();
+ await interestsResult.fold(
+ (err) async {
+ debugPrint("Failed to fetch interests");
+ },
+ (list) async {
+ final updated = user.copyWith(interests: list.toSet());
+ ref.read(currentUserProvider.notifier).adduser(updated);
+ await _authLocalRepository.saveUser(updated);
+ },
+ );
+
+ state = AuthState.authenticated('github_login_success');
_registerFcmToken();
_listenForFcmTokenRefresh();
},
diff --git a/lib/features/auth/view_model/auth_view_model.g.dart b/lib/features/auth/view_model/auth_view_model.g.dart
index 1109c12..f254afe 100644
--- a/lib/features/auth/view_model/auth_view_model.g.dart
+++ b/lib/features/auth/view_model/auth_view_model.g.dart
@@ -41,7 +41,7 @@ final class AuthViewModelProvider
}
}
-String _$authViewModelHash() => r'924e5a98fc7048caca4ebb4164728767ec8beae9';
+String _$authViewModelHash() => r'739d52fc0da76e94aa632248f2f291fb6b2783d1';
abstract class _$AuthViewModel extends $Notifier {
AuthState build();
diff --git a/lib/features/chat/repositories/chat_local_repository.dart b/lib/features/chat/repositories/chat_local_repository.dart
index 5d5669e..befe0c4 100644
--- a/lib/features/chat/repositories/chat_local_repository.dart
+++ b/lib/features/chat/repositories/chat_local_repository.dart
@@ -10,10 +10,18 @@ ChatLocalRepository chatLocalRepository(Ref ref) {
}
class ChatLocalRepository {
- final Box _conversationsBox = Hive.box(
- "conversationsBox",
- );
- final Box _messagesBox = Hive.box("messagesBox");
+ final Box _conversationsBox;
+ final Box _messagesBox;
+
+ ChatLocalRepository()
+ : _conversationsBox = Hive.box("conversationsBox"),
+ _messagesBox = Hive.box("messagesBox");
+
+ ChatLocalRepository.forTesting({
+ required Box conversationsBox,
+ required Box messagesBox,
+ }) : _conversationsBox = conversationsBox,
+ _messagesBox = messagesBox;
List getCachedMessages(String chatId) {
final messages = _messagesBox.values
@@ -64,7 +72,8 @@ class ChatLocalRepository {
for (var msg in messagesToUpdate) {
msg.status = "READ";
- await msg.save();
+ // await msg.save();
+ await _messagesBox.put(msg.id, msg); //
}
}
@@ -72,7 +81,8 @@ class ChatLocalRepository {
final msg = _messagesBox.get(messageId);
if (msg != null && msg.status != "READ") {
msg.status = "SENT";
- await msg.save();
+ // await msg.save();
+ await _messagesBox.put(msg.id, msg); //
}
}
diff --git a/lib/features/chat/repositories/socket_repository.dart b/lib/features/chat/repositories/socket_repository.dart
index 2133003..70cd91f 100644
--- a/lib/features/chat/repositories/socket_repository.dart
+++ b/lib/features/chat/repositories/socket_repository.dart
@@ -137,13 +137,13 @@ class SocketRepository {
if (data != null && !_unseenChatsController.isClosed) {
_unseenChatsController.add(Map.from(data));
}
- }); // New listener for unseen chats count
+ });
}
void sendOpenMessageTab() {
print("sending open-message-tab");
_socket?.emit('open-message-tab');
- } // to be zero when user opens chat tab
+ }
void sendTyping(String chatId, bool isTyping) {
_socket?.emit('typing', {'chatId': chatId, 'isTyping': isTyping});
diff --git a/lib/features/chat/view/screens/Search_User_Group.dart b/lib/features/chat/view/screens/Search_User_Group.dart
index 4791cea..5bdc302 100644
--- a/lib/features/chat/view/screens/Search_User_Group.dart
+++ b/lib/features/chat/view/screens/Search_User_Group.dart
@@ -1,14 +1,14 @@
import 'dart:async';
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/chat/models/usersearchmodel.dart';
import 'package:lite_x/features/chat/providers/searchResultsProvider.dart';
import 'package:lite_x/features/chat/view_model/conversions/Conversations_view_model.dart';
+import 'package:lite_x/features/profile/models/shared.dart';
class SearchUserGroup extends ConsumerStatefulWidget {
const SearchUserGroup({super.key});
@@ -227,16 +227,9 @@ class _SearchUserGroupState extends ConsumerState {
(u) => u.id == user.id,
);
return ListTile(
- leading: CircleAvatar(
- backgroundColor: const Color(0xFF1E2732),
-
- backgroundImage: isValidHttpUrl(user.profileMedia)
- ? CachedNetworkImageProvider(user.profileMedia!)
- : null,
-
- child: !isValidHttpUrl(user.profileMedia)
- ? const Icon(Icons.person, color: Colors.grey)
- : null,
+ leading: BuildSmallProfileImage(
+ radius: 24,
+ username: user.username,
),
title: Text(
diff --git a/lib/features/chat/view/screens/chat_Screen.dart b/lib/features/chat/view/screens/chat_Screen.dart
index ff1c6ef..6f37a71 100644
--- a/lib/features/chat/view/screens/chat_Screen.dart
+++ b/lib/features/chat/view/screens/chat_Screen.dart
@@ -1,3 +1,5 @@
+// ignore_for_file: unused_result
+
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
@@ -10,12 +12,14 @@ import 'package:lite_x/features/chat/view/widgets/chat/TypingIndicator.dart';
import 'package:lite_x/features/chat/view/widgets/chat/message_input_bar.dart';
import 'package:lite_x/features/chat/view_model/chat/Chat_view_model.dart';
import 'package:lite_x/features/chat/view_model/conversions/Conversations_view_model.dart';
+import 'package:lite_x/features/profile/models/shared.dart';
+import 'package:lite_x/features/profile/view_model/providers.dart';
class ChatScreen extends ConsumerStatefulWidget {
final String chatId;
final String title; // name of user
final String? subtitle; // username
- final String? profileImage;
+ final String? profileImage; // media id of receiver
final bool isGroup;
final int? recipientFollowersCount;
const ChatScreen({
@@ -49,9 +53,8 @@ class _ChatScreenState extends ConsumerState {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
-
_setupChatSubscription();
-
+ ref.refresh(followersProvider(widget.subtitle ?? ""));
ref.read(chatViewModelProvider.notifier).loadChat(widget.chatId);
ref.read(activeChatProvider.notifier).setActive(widget.chatId);
ref
@@ -183,7 +186,18 @@ class _ChatScreenState extends ConsumerState {
Widget build(BuildContext context) {
final chatState = ref.watch(chatViewModelProvider);
final currentUser = ref.watch(currentUserProvider);
-
+ final followersAsync = ref.watch(followersProvider(widget.subtitle ?? ""));
+ int latestFollowersCount = widget.recipientFollowersCount ?? 0;
+ followersAsync.when(
+ data: (either) {
+ latestFollowersCount = either.fold(
+ (_) => latestFollowersCount,
+ (users) => users.length,
+ );
+ },
+ loading: () {},
+ error: (_, __) {},
+ );
if (currentUser == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
@@ -226,7 +240,7 @@ class _ChatScreenState extends ConsumerState {
}
if (index == messages.length) {
- return _buildProfileHeader();
+ return _buildProfileHeader(latestFollowersCount);
}
final message = messages[messages.length - index - 1];
@@ -288,31 +302,14 @@ class _ChatScreenState extends ConsumerState {
);
}
- Widget _buildProfileHeader() {
+ Widget _buildProfileHeader(int followersCount) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- CircleAvatar(
- radius: 45,
- backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
- backgroundImage: widget.profileImage != null
- ? NetworkImage(widget.profileImage!)
- : null,
- child: widget.profileImage == null
- ? Text(
- widget.title[0].toUpperCase(),
- style: TextStyle(
- fontSize: 48,
- fontWeight: FontWeight.bold,
- color: Theme.of(context).primaryColor,
- ),
- )
- : null,
- ),
+ BuildSmallProfileImage(radius: 48, username: widget.subtitle),
const SizedBox(height: 8),
-
Text(
widget.title,
style: const TextStyle(
@@ -323,20 +320,17 @@ class _ChatScreenState extends ConsumerState {
),
if (widget.subtitle != null) ...[
- const SizedBox(height: 2),
Text(
'@${widget.subtitle}',
style: const TextStyle(fontSize: 15, color: Color(0xFF858B91)),
),
],
- if (widget.recipientFollowersCount != null) ...[
- const SizedBox(height: 8),
- Text(
- '${widget.recipientFollowersCount} Followers',
- style: TextStyle(fontSize: 14, color: Colors.grey[600]),
- ),
- ],
+ const SizedBox(height: 8),
+ Text(
+ '${followersCount} Followers',
+ style: TextStyle(fontSize: 14, color: Color(0xFF858B91)),
+ ),
const SizedBox(height: 20),
@@ -347,14 +341,14 @@ class _ChatScreenState extends ConsumerState {
endIndent: 20,
),
- const SizedBox(height: 8),
+ const SizedBox(height: 4),
Text(
_getConversationStartDate(),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
- color: Colors.grey[600],
+ color: Colors.white,
),
),
],
@@ -379,7 +373,7 @@ class _ChatScreenState extends ConsumerState {
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
- return '${messageDate.day}/${messageDate.month}/${messageDate.year}';
+ return '${messageDate.day},${messageDate.month},${messageDate.year}';
}
}
diff --git a/lib/features/chat/view/screens/conversations_screen.dart b/lib/features/chat/view/screens/conversations_screen.dart
index 5553676..dd859ab 100644
--- a/lib/features/chat/view/screens/conversations_screen.dart
+++ b/lib/features/chat/view/screens/conversations_screen.dart
@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/chat/view/widgets/conversion/conversations_list.dart';
import 'package:lite_x/features/chat/view/widgets/conversion/conversion_app_bar.dart';
+import 'package:lite_x/features/home/view/widgets/profile_side_drawer.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
class ConversationsScreen extends StatelessWidget {
@@ -12,6 +13,7 @@ class ConversationsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
+ drawer: const ProfileSideDrawer(),
backgroundColor: Palette.background,
appBar: const ConversationAppBar(),
body: const ConversationsList(),
diff --git a/lib/features/chat/view/widgets/chat/MessageAppBar.dart b/lib/features/chat/view/widgets/chat/MessageAppBar.dart
index 8c6de0f..e30cc37 100644
--- a/lib/features/chat/view/widgets/chat/MessageAppBar.dart
+++ b/lib/features/chat/view/widgets/chat/MessageAppBar.dart
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
+import 'package:lite_x/features/profile/models/shared.dart';
class MessageAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
- final String? profileImage;
final String subtitle;
final VoidCallback? onProfileTap;
@@ -11,7 +11,6 @@ class MessageAppBar extends StatelessWidget implements PreferredSizeWidget {
super.key,
required this.title,
required this.subtitle,
- this.profileImage,
this.onProfileTap,
});
@@ -31,15 +30,7 @@ class MessageAppBar extends StatelessWidget implements PreferredSizeWidget {
const SizedBox(width: 12),
Hero(
tag: "message_app_bar",
- child: CircleAvatar(
- radius: 18,
- backgroundImage: profileImage != null
- ? NetworkImage(profileImage!)
- : null,
- child: profileImage == null
- ? const Icon(Icons.person, color: Colors.white, size: 20)
- : null,
- ),
+ child: BuildSmallProfileImage(radius: 18, username: subtitle),
),
const SizedBox(width: 12),
Expanded(
diff --git a/lib/features/chat/view/widgets/chat/MessageBubble.dart b/lib/features/chat/view/widgets/chat/MessageBubble.dart
index 7dacbc2..2ef3767 100644
--- a/lib/features/chat/view/widgets/chat/MessageBubble.dart
+++ b/lib/features/chat/view/widgets/chat/MessageBubble.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/chat/models/messagemodel.dart';
class MessageBubble extends StatelessWidget {
diff --git a/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart b/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart
index 585017b..629ed92 100644
--- a/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart
+++ b/lib/features/chat/view/widgets/chat/MessageOptionsSheet.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/chat/models/messagemodel.dart';
class MessageOptionsSheet extends StatelessWidget {
diff --git a/lib/features/chat/view/widgets/chat/TypingIndicator.dart b/lib/features/chat/view/widgets/chat/TypingIndicator.dart
index b721226..f5f4097 100644
--- a/lib/features/chat/view/widgets/chat/TypingIndicator.dart
+++ b/lib/features/chat/view/widgets/chat/TypingIndicator.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class TypingIndicator extends StatefulWidget {
final String userName;
diff --git a/lib/features/chat/view/widgets/chat/message_input_bar.dart b/lib/features/chat/view/widgets/chat/message_input_bar.dart
index 9f22514..77eba4d 100644
--- a/lib/features/chat/view/widgets/chat/message_input_bar.dart
+++ b/lib/features/chat/view/widgets/chat/message_input_bar.dart
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class MessageInputBar extends ConsumerStatefulWidget {
final Function(String text) onSendMessage;
diff --git a/lib/features/chat/view/widgets/conversion/conversation_tile.dart b/lib/features/chat/view/widgets/conversion/conversation_tile.dart
index 6ca71ad..4f42007 100644
--- a/lib/features/chat/view/widgets/conversion/conversation_tile.dart
+++ b/lib/features/chat/view/widgets/conversion/conversation_tile.dart
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
+import 'package:lite_x/features/profile/models/shared.dart';
+import 'package:lite_x/features/profile/view/screens/profile_screen.dart';
class ConversationTile extends StatelessWidget {
final String recipientId;
@@ -31,6 +33,15 @@ class ConversationTile extends StatelessWidget {
this.recipientFollowersCount = 0,
this.onLongPress,
});
+ void _openProfile(BuildContext context, String username) {
+ final normalized = username.startsWith('@')
+ ? username.substring(1)
+ : username;
+
+ Navigator.of(context).push(
+ MaterialPageRoute(builder: (_) => ProfilePage(username: normalized)),
+ );
+ }
@override
Widget build(BuildContext context) {
@@ -57,19 +68,15 @@ class ConversationTile extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- CircleAvatar(
- radius: 24,
- backgroundImage: avatarUrl != null
- ? NetworkImage(avatarUrl!)
- : null,
- backgroundColor: Palette.cardBackground,
- child: avatarUrl == null
- ? const Icon(
- Icons.person_3_rounded,
- color: Palette.textSecondary,
- )
- : null,
+ GestureDetector(
+ onTap: () {
+ if (username.isNotEmpty) {
+ _openProfile(context, username);
+ }
+ },
+ child: BuildSmallProfileImage(radius: 24, username: username),
),
+
const SizedBox(width: 12),
Expanded(
diff --git a/lib/features/chat/view/widgets/conversion/conversations_list.dart b/lib/features/chat/view/widgets/conversion/conversations_list.dart
index 967bcd5..35cb35c 100644
--- a/lib/features/chat/view/widgets/conversion/conversations_list.dart
+++ b/lib/features/chat/view/widgets/conversion/conversations_list.dart
@@ -150,7 +150,6 @@ class _ConversationsListState extends ConsumerState {
@override
Widget build(BuildContext context) {
final conversationsAsync = ref.watch(conversationsViewModelProvider);
-
return conversationsAsync.when(
data: (conversations) {
if (conversations.isEmpty) {
diff --git a/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart b/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart
index 5750a48..c2983e4 100644
--- a/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart
+++ b/lib/features/chat/view/widgets/conversion/conversion_app_bar.dart
@@ -1,8 +1,8 @@
-import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/theme/Palette.dart';
+import 'package:lite_x/features/profile/models/shared.dart';
class ConversationAppBar extends ConsumerWidget implements PreferredSizeWidget {
const ConversationAppBar({super.key});
@@ -15,6 +15,7 @@ class ConversationAppBar extends ConsumerWidget implements PreferredSizeWidget {
final currentuser = ref.watch(currentUserProvider);
return AppBar(
+ automaticallyImplyLeading: false,
backgroundColor: Palette.background,
elevation: 0,
titleSpacing: 0,
@@ -24,22 +25,17 @@ class ConversationAppBar extends ConsumerWidget implements PreferredSizeWidget {
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
- child: GestureDetector(
- onTap: () {},
- child: Hero(
- tag: "chat_user_avatar",
- child: CircleAvatar(
- radius: 18,
- backgroundImage: currentuser?.localProfilePhotoPath != null
- ? FileImage(File(currentuser!.localProfilePhotoPath!))
- : null,
- child: currentuser?.photo == null
- ? const Icon(
- Icons.person,
- color: Colors.white,
- size: 20,
- )
- : null,
+ child: Builder(
+ builder: (context) => GestureDetector(
+ onTap: () {
+ Scaffold.of(context).openDrawer();
+ },
+ child: Hero(
+ tag: "chat_user_avatar",
+ child: BuildSmallProfileImage(
+ radius: 20,
+ username: currentuser?.username,
+ ),
),
),
),
diff --git a/lib/features/chat/view/widgets/conversion/empty_inbox.dart b/lib/features/chat/view/widgets/conversion/empty_inbox.dart
index 91f695d..27f02f1 100644
--- a/lib/features/chat/view/widgets/conversion/empty_inbox.dart
+++ b/lib/features/chat/view/widgets/conversion/empty_inbox.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class EmptyInbox extends StatelessWidget {
const EmptyInbox({super.key});
diff --git a/lib/features/chat/view_model/conversions/Conversations_view_model.dart b/lib/features/chat/view_model/conversions/Conversations_view_model.dart
index b4f00ff..e9bb319 100644
--- a/lib/features/chat/view_model/conversions/Conversations_view_model.dart
+++ b/lib/features/chat/view_model/conversions/Conversations_view_model.dart
@@ -15,17 +15,25 @@ part 'Conversations_view_model.g.dart';
@Riverpod(keepAlive: true)
class ConversationsViewModel extends _$ConversationsViewModel {
- late final ChatRemoteRepository _chatRemoteRepository;
- late final ChatLocalRepository _chatLocalRepository;
- late final SocketRepository _socketRepository;
+ // late final ChatRemoteRepository _chatRemoteRepository;
+ // late final ChatLocalRepository _chatLocalRepository;
+ // late final SocketRepository _socketRepository;
UserModel? _currentUser;
bool _listening = false;
StreamSubscription? _messageSub;
+ ChatRemoteRepository get _chatRemoteRepository =>
+ ref.watch(chatRemoteRepositoryProvider);
+
+ ChatLocalRepository get _chatLocalRepository =>
+ ref.watch(chatLocalRepositoryProvider);
+
+ SocketRepository get _socketRepository => ref.watch(socketRepositoryProvider);
+
@override
AsyncValue> build() {
- _chatRemoteRepository = ref.watch(chatRemoteRepositoryProvider);
- _chatLocalRepository = ref.watch(chatLocalRepositoryProvider);
- _socketRepository = ref.watch(socketRepositoryProvider);
+ // _chatRemoteRepository = ref.watch(chatRemoteRepositoryProvider);
+ // _chatLocalRepository = ref.watch(chatLocalRepositoryProvider);
+ // _socketRepository = ref.watch(socketRepositoryProvider);
_currentUser = ref.watch(currentUserProvider);
if (!_listening) {
_listenToNewMessages();
@@ -294,7 +302,13 @@ class ConversationsViewModel extends _$ConversationsViewModel {
);
} catch (e, st) {
print("Conversation Load Failed: $e");
- state = AsyncValue.error(e, st);
+ final cached = _chatLocalRepository.getAllConversations();
+ if (cached.isNotEmpty) {
+ print("Loading cached conversations because server failed");
+ state = AsyncValue.data([...cached]);
+ } else {
+ state = AsyncValue.error(e, st);
+ }
}
}
diff --git a/lib/features/chat/view_model/conversions/Conversations_view_model.g.dart b/lib/features/chat/view_model/conversions/Conversations_view_model.g.dart
index f6fe94f..7ecdf03 100644
--- a/lib/features/chat/view_model/conversions/Conversations_view_model.g.dart
+++ b/lib/features/chat/view_model/conversions/Conversations_view_model.g.dart
@@ -48,7 +48,7 @@ final class ConversationsViewModelProvider
}
String _$conversationsViewModelHash() =>
- r'bfee74aa41c86afff7fea22093b0cb68b113c1c2';
+ r'39a1625fd2be7315ebde79b9ae5f55138f1291a2';
abstract class _$ConversationsViewModel
extends $Notifier>> {
diff --git a/lib/features/explore/view/explore_screen.dart b/lib/features/explore/view/explore_screen.dart
index 62479ab..275d275 100644
--- a/lib/features/explore/view/explore_screen.dart
+++ b/lib/features/explore/view/explore_screen.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import '../view_model/explore_view_model.dart';
import '../view_model/explore_state.dart';
import '../widgets/explore_nav_bar.dart';
@@ -37,12 +37,19 @@ class _ForYouItem {
this.user,
});
- factory _ForYouItem.header(String title) => _ForYouItem._(type: _ForYouItemType.header, title: title);
+ factory _ForYouItem.header(String title) =>
+ _ForYouItem._(type: _ForYouItemType.header, title: title);
factory _ForYouItem.divider() => _ForYouItem._(type: _ForYouItemType.divider);
- factory _ForYouItem.todayNews(TrendModel trend) => _ForYouItem._(type: _ForYouItemType.todayNews, trend: trend);
- factory _ForYouItem.trendingCountry(TrendModel trend) => _ForYouItem._(type: _ForYouItemType.trendingCountry, trend: trend);
- factory _ForYouItem.whoToFollow(WhoToFollowModel user) => _ForYouItem._(type: _ForYouItemType.whoToFollow, user: user);
- factory _ForYouItem.categoryTweet(String category, SuggestedTweetModel tweet) => _ForYouItem._(type: _ForYouItemType.categoryTweet, tweet: tweet);
+ factory _ForYouItem.todayNews(TrendModel trend) =>
+ _ForYouItem._(type: _ForYouItemType.todayNews, trend: trend);
+ factory _ForYouItem.trendingCountry(TrendModel trend) =>
+ _ForYouItem._(type: _ForYouItemType.trendingCountry, trend: trend);
+ factory _ForYouItem.whoToFollow(WhoToFollowModel user) =>
+ _ForYouItem._(type: _ForYouItemType.whoToFollow, user: user);
+ factory _ForYouItem.categoryTweet(
+ String category,
+ SuggestedTweetModel tweet,
+ ) => _ForYouItem._(type: _ForYouItemType.categoryTweet, tweet: tweet);
}
class ExploreScreen extends ConsumerWidget {
@@ -70,66 +77,65 @@ class ExploreScreen extends ConsumerWidget {
Expanded(
child: state.isLoading
? const Center(
- child: CircularProgressIndicator(
- color: Palette.primary,
- ),
+ child: CircularProgressIndicator(color: Palette.primary),
)
: state.error != null
- ? Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(
- Icons.error_outline,
- color: Palette.error,
- size: 48,
- ),
- const SizedBox(height: 16),
- Text(
- state.error!,
- style: const TextStyle(
- color: Palette.textSecondary,
- fontSize: 16,
- ),
- ),
- const SizedBox(height: 16),
- ElevatedButton(
- onPressed: () {
- // Retry loading
- },
- child: const Text('Retry'),
- ),
- ],
+ ? Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.error_outline,
+ color: Palette.error,
+ size: 48,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ state.error!,
+ style: const TextStyle(
+ color: Palette.textSecondary,
+ fontSize: 16,
+ ),
+ ),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: () {
+ // Retry loading
+ },
+ child: const Text('Retry'),
),
- )
- : state.selectedCategory == ExploreCategory.forYou
- ? _buildForYouContent(state, viewModel)
- : _buildContent(state, viewModel),
+ ],
+ ),
+ )
+ : state.selectedCategory == ExploreCategory.forYou
+ ? _buildForYouContent(state, viewModel)
+ : _buildContent(state, viewModel),
),
],
),
);
}
- Widget _buildContent(
- ExploreState state,
- ExploreViewModel viewModel,
- ) {
+ Widget _buildContent(ExploreState state, ExploreViewModel viewModel) {
// Trending tab should only show trend cards, no suggested tweets
final isTrendingTab = state.selectedCategory == ExploreCategory.trending;
-
+
// Show suggested tweets after 5-6 trend cards (but not for Trending tab)
const int trendsBeforeTweets = 5;
- final showTweetsSection = !isTrendingTab &&
- state.trends.length >= trendsBeforeTweets &&
- state.suggestedTweets.isNotEmpty;
-
+ final showTweetsSection =
+ !isTrendingTab &&
+ state.trends.length >= trendsBeforeTweets &&
+ state.suggestedTweets.isNotEmpty;
+
return ListView.builder(
cacheExtent: 500,
addAutomaticKeepAlives: false,
addRepaintBoundaries: true,
- itemCount: state.trends.length +
- (showTweetsSection ? state.suggestedTweets.length + 1 : 0), // +1 for header
+ itemCount:
+ state.trends.length +
+ (showTweetsSection
+ ? state.suggestedTweets.length + 1
+ : 0), // +1 for header
itemBuilder: (context, index) {
// Show trends first (up to trendsBeforeTweets)
if (index < state.trends.length) {
@@ -140,13 +146,13 @@ class ExploreScreen extends ConsumerWidget {
children: [
// Section header
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
decoration: BoxDecoration(
border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
+ bottom: BorderSide(color: Palette.divider, width: 1),
),
),
child: const Text(
@@ -159,22 +165,22 @@ class ExploreScreen extends ConsumerWidget {
),
),
// First suggested tweet
- SuggestedTweetCard(
- tweet: state.suggestedTweets[0],
- ),
+ SuggestedTweetCard(tweet: state.suggestedTweets[0]),
],
);
}
-
+
final trend = state.trends[index];
// Use enhanced card only for Entertainment, Sports, and News categories
// Trending tab should only show regular trend cards
- final useEnhancedCard = state.selectedCategory != ExploreCategory.trending &&
+ final useEnhancedCard =
+ state.selectedCategory != ExploreCategory.trending &&
(state.selectedCategory == ExploreCategory.entertainment ||
- state.selectedCategory == ExploreCategory.sports ||
- state.selectedCategory == ExploreCategory.news);
-
- if (useEnhancedCard && (trend.headline != null || trend.avatarUrls != null)) {
+ state.selectedCategory == ExploreCategory.sports ||
+ state.selectedCategory == ExploreCategory.news);
+
+ if (useEnhancedCard &&
+ (trend.headline != null || trend.avatarUrls != null)) {
return EnhancedTrendCard(
key: ValueKey('enhanced_trend_${trend.id}'),
trend: trend,
@@ -183,7 +189,7 @@ class ExploreScreen extends ConsumerWidget {
},
);
}
-
+
return TrendCard(
key: ValueKey('trend_${trend.id}'),
trend: trend,
@@ -192,31 +198,30 @@ class ExploreScreen extends ConsumerWidget {
},
);
}
-
+
// Show remaining suggested tweets
if (showTweetsSection) {
final tweetIndex = index - state.trends.length - 1; // -1 for header
if (tweetIndex >= 0 && tweetIndex < state.suggestedTweets.length) {
return SuggestedTweetCard(
- key: ValueKey('suggested_tweet_${state.suggestedTweets[tweetIndex].id}'),
+ key: ValueKey(
+ 'suggested_tweet_${state.suggestedTweets[tweetIndex].id}',
+ ),
tweet: state.suggestedTweets[tweetIndex],
);
}
}
-
+
return const SizedBox.shrink();
},
);
}
- Widget _buildForYouContent(
- ExploreState state,
- ExploreViewModel viewModel,
- ) {
+ Widget _buildForYouContent(ExploreState state, ExploreViewModel viewModel) {
// Build list of all items with their types for efficient building
// This is cached and only rebuilt when state changes
final items = <_ForYouItem>[];
-
+
// Section 1: Today's News
if (state.todaysNews.isNotEmpty) {
items.add(_ForYouItem.header('Today\'s News'));
@@ -282,10 +287,7 @@ class ExploreScreen extends ConsumerWidget {
key: ValueKey('who_follow_${item.user!.id}'),
decoration: BoxDecoration(
border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
+ bottom: BorderSide(color: Palette.divider, width: 1),
),
),
child: WhoToFollowCard(
@@ -312,12 +314,7 @@ class ExploreScreen extends ConsumerWidget {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
- ),
+ border: Border(bottom: BorderSide(color: Palette.divider, width: 1)),
),
child: Text(
title,
@@ -332,9 +329,6 @@ class ExploreScreen extends ConsumerWidget {
}
Widget _buildSectionDivider() {
- return const SizedBox(
- height: 12,
- );
+ return const SizedBox(height: 12);
}
}
-
diff --git a/lib/features/explore/widgets/category_tabs.dart b/lib/features/explore/widgets/category_tabs.dart
index f74d97a..d7074fa 100644
--- a/lib/features/explore/widgets/category_tabs.dart
+++ b/lib/features/explore/widgets/category_tabs.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import '../view_model/explore_state.dart';
class CategoryTabs extends StatelessWidget {
@@ -33,60 +33,58 @@ class CategoryTabs extends StatelessWidget {
return Container(
decoration: BoxDecoration(
color: Palette.background,
- border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
- ),
+ border: Border(bottom: BorderSide(color: Palette.divider, width: 1)),
),
height: 48,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: categories.map((category) {
- final isSelected = category == selectedCategory;
+ final isSelected = category == selectedCategory;
- return Expanded(
- child: Material(
- color: Colors.transparent,
- child: InkWell(
- onTap: () => onCategorySelected(category),
- splashColor: Colors.transparent,
- highlightColor: Colors.transparent,
- child: Container(
- alignment: Alignment.center,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- categoryLabels[category]!,
- style: TextStyle(
- fontSize: 15,
- fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
- color: isSelected ? Palette.textPrimary : Palette.textSecondary,
- ),
- overflow: TextOverflow.ellipsis,
- ),
- const SizedBox(height: 4),
- Container(
- height: 3,
- width: isSelected ? 30 : 0,
- decoration: BoxDecoration(
- color: Palette.primary,
- borderRadius: BorderRadius.circular(2),
- ),
- ),
- ],
+ return Expanded(
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => onCategorySelected(category),
+ splashColor: Colors.transparent,
+ highlightColor: Colors.transparent,
+ child: Container(
+ alignment: Alignment.center,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ categoryLabels[category]!,
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: isSelected
+ ? FontWeight.bold
+ : FontWeight.normal,
+ color: isSelected
+ ? Palette.textPrimary
+ : Palette.textSecondary,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 4),
+ Container(
+ height: 3,
+ width: isSelected ? 30 : 0,
+ decoration: BoxDecoration(
+ color: Palette.primary,
+ borderRadius: BorderRadius.circular(2),
+ ),
),
- ),
+ ],
),
),
- );
- }).toList(),
+ ),
+ ),
+ );
+ }).toList(),
),
),
);
}
}
-
diff --git a/lib/features/explore/widgets/enhanced_trend_card.dart b/lib/features/explore/widgets/enhanced_trend_card.dart
index af429f8..07a37e2 100644
--- a/lib/features/explore/widgets/enhanced_trend_card.dart
+++ b/lib/features/explore/widgets/enhanced_trend_card.dart
@@ -1,16 +1,12 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import '../models/trend_model.dart';
class EnhancedTrendCard extends StatelessWidget {
final TrendModel trend;
final VoidCallback? onTap;
- const EnhancedTrendCard({
- super.key,
- required this.trend,
- this.onTap,
- });
+ const EnhancedTrendCard({super.key, required this.trend, this.onTap});
String _formatPostCount(int count) {
if (count >= 1000000) {
@@ -24,19 +20,16 @@ class EnhancedTrendCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
- onTap: onTap ?? () {
- // Navigate to trend timeline
- },
+ onTap:
+ onTap ??
+ () {
+ // Navigate to trend timeline
+ },
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
- ),
+ border: Border(bottom: BorderSide(color: Palette.divider, width: 1)),
),
child: Material(
color: Colors.transparent,
@@ -53,123 +46,125 @@ class EnhancedTrendCard extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Left Content Section
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Headline
- Text(
- trend.headline ?? trend.title,
- style: const TextStyle(
- fontSize: 15,
- fontWeight: FontWeight.w600,
- color: Palette.textPrimary,
- height: 1.3,
- ),
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- ),
- const SizedBox(height: 12),
- // Avatars Row and Metadata in one row
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
+ // Left Content Section
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Avatars Row
- if (trend.avatarUrls != null && trend.avatarUrls!.isNotEmpty) ...[
- _buildAvatarRow(trend.avatarUrls!),
- const SizedBox(width: 12),
- ],
- // Metadata Line
- Expanded(
- child: Row(
- children: [
- if (trend.timestamp != null) ...[
- Text(
- trend.timestamp!,
- style: const TextStyle(
- fontSize: 13,
- color: Palette.textSecondary,
- ),
- ),
- const Text(
- ' · ',
- style: TextStyle(
- fontSize: 13,
- color: Palette.textSecondary,
- ),
- ),
- ],
- if (trend.category != null) ...[
- Text(
- trend.category!,
- style: const TextStyle(
- fontSize: 13,
- color: Palette.textSecondary,
- ),
- ),
- const Text(
- ' · ',
- style: TextStyle(
- fontSize: 13,
- color: Palette.textSecondary,
+ // Headline
+ Text(
+ trend.headline ?? trend.title,
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ color: Palette.textPrimary,
+ height: 1.3,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 12),
+ // Avatars Row and Metadata in one row
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ // Avatars Row
+ if (trend.avatarUrls != null &&
+ trend.avatarUrls!.isNotEmpty) ...[
+ _buildAvatarRow(trend.avatarUrls!),
+ const SizedBox(width: 12),
+ ],
+ // Metadata Line
+ Expanded(
+ child: Row(
+ children: [
+ if (trend.timestamp != null) ...[
+ Text(
+ trend.timestamp!,
+ style: const TextStyle(
+ fontSize: 13,
+ color: Palette.textSecondary,
+ ),
+ ),
+ const Text(
+ ' · ',
+ style: TextStyle(
+ fontSize: 13,
+ color: Palette.textSecondary,
+ ),
+ ),
+ ],
+ if (trend.category != null) ...[
+ Text(
+ trend.category!,
+ style: const TextStyle(
+ fontSize: 13,
+ color: Palette.textSecondary,
+ ),
+ ),
+ const Text(
+ ' · ',
+ style: TextStyle(
+ fontSize: 13,
+ color: Palette.textSecondary,
+ ),
+ ),
+ ],
+ Text(
+ '${_formatPostCount(trend.postCount)} posts',
+ style: const TextStyle(
+ fontSize: 13,
+ color: Palette.textSecondary,
+ ),
),
- ),
- ],
- Text(
- '${_formatPostCount(trend.postCount)} posts',
- style: const TextStyle(
- fontSize: 13,
- color: Palette.textSecondary,
- ),
+ ],
),
- ],
- ),
+ ),
+ ],
),
],
),
- ],
),
- ),
- const SizedBox(width: 12),
- // Right Content Section (Thumbnail)
- if (trend.imageUrl != null && trend.imageUrl!.isNotEmpty)
- ClipRRect(
- borderRadius: BorderRadius.circular(8),
- child: Image.network(
- trend.imageUrl!,
- width: 72,
- height: 72,
- fit: BoxFit.cover,
- errorBuilder: (context, error, stackTrace) => Container(
+ const SizedBox(width: 12),
+ // Right Content Section (Thumbnail)
+ if (trend.imageUrl != null && trend.imageUrl!.isNotEmpty)
+ ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: Image.network(
+ trend.imageUrl!,
width: 72,
height: 72,
- decoration: BoxDecoration(
- color: Palette.cardBackground,
- borderRadius: BorderRadius.circular(8),
- ),
- child: const Icon(
- Icons.image,
- color: Palette.icons,
- size: 24,
- ),
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) =>
+ Container(
+ width: 72,
+ height: 72,
+ decoration: BoxDecoration(
+ color: Palette.cardBackground,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Icon(
+ Icons.image,
+ color: Palette.icons,
+ size: 24,
+ ),
+ ),
+ ),
+ )
+ else
+ Container(
+ width: 72,
+ height: 72,
+ decoration: BoxDecoration(
+ color: Palette.cardBackground,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: const Icon(
+ Icons.image,
+ color: Palette.icons,
+ size: 24,
),
),
- )
- else
- Container(
- width: 72,
- height: 72,
- decoration: BoxDecoration(
- color: Palette.cardBackground,
- borderRadius: BorderRadius.circular(8),
- ),
- child: const Icon(
- Icons.image,
- color: Palette.icons,
- size: 24,
- ),
- ),
],
),
),
@@ -183,7 +178,7 @@ class EnhancedTrendCard extends StatelessWidget {
Widget _buildAvatarRow(List avatarUrls) {
// Show maximum 3 avatars, overlapping by 12px
final avatarsToShow = avatarUrls.take(3).toList();
-
+
// Calculate width: first avatar (24px) + overlap spacing (12px * (count - 1))
final totalWidth = 24.0 + (12.0 * (avatarsToShow.length - 1));
@@ -195,7 +190,7 @@ class EnhancedTrendCard extends StatelessWidget {
children: avatarsToShow.asMap().entries.map((entry) {
final index = entry.key;
final avatarUrl = entry.value;
-
+
return Positioned(
left: index * 12.0, // Overlap by 12px
child: Container(
@@ -203,10 +198,7 @@ class EnhancedTrendCard extends StatelessWidget {
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
- border: Border.all(
- color: Palette.background,
- width: 1.5,
- ),
+ border: Border.all(color: Palette.background, width: 1.5),
color: Palette.cardBackground,
),
child: avatarUrl.isNotEmpty
@@ -216,18 +208,15 @@ class EnhancedTrendCard extends StatelessWidget {
width: 24,
height: 24,
fit: BoxFit.cover,
- errorBuilder: (context, error, stackTrace) => const Icon(
- Icons.person,
- size: 14,
- color: Palette.icons,
- ),
+ errorBuilder: (context, error, stackTrace) =>
+ const Icon(
+ Icons.person,
+ size: 14,
+ color: Palette.icons,
+ ),
),
)
- : const Icon(
- Icons.person,
- size: 14,
- color: Palette.icons,
- ),
+ : const Icon(Icons.person, size: 14, color: Palette.icons),
),
);
}).toList(),
@@ -235,4 +224,3 @@ class EnhancedTrendCard extends StatelessWidget {
);
}
}
-
diff --git a/lib/features/explore/widgets/explore_nav_bar.dart b/lib/features/explore/widgets/explore_nav_bar.dart
index e6e47fa..bb8ba37 100644
--- a/lib/features/explore/widgets/explore_nav_bar.dart
+++ b/lib/features/explore/widgets/explore_nav_bar.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class ExploreNavBar extends StatelessWidget {
final String? userAvatarUrl;
@@ -21,25 +21,23 @@ class ExploreNavBar extends StatelessWidget {
return Container(
decoration: BoxDecoration(
color: Palette.background,
- border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
- ),
+ border: Border(bottom: BorderSide(color: Palette.divider, width: 1)),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// User Avatar
GestureDetector(
- onTap: onAvatarTap ?? () {
- // Open profile drawer or navigate to profile
- },
+ onTap:
+ onAvatarTap ??
+ () {
+ // Open profile drawer or navigate to profile
+ },
child: CircleAvatar(
radius: 18,
backgroundColor: Palette.primary,
- backgroundImage: userAvatarUrl != null && userAvatarUrl!.isNotEmpty
+ backgroundImage:
+ userAvatarUrl != null && userAvatarUrl!.isNotEmpty
? NetworkImage(userAvatarUrl!)
: null,
child: userAvatarUrl == null || userAvatarUrl!.isEmpty
@@ -96,15 +94,13 @@ class ExploreNavBar extends StatelessWidget {
// Settings Icon
IconButton(
- icon: const Icon(
- Icons.tune,
- size: 24,
- color: Palette.icons,
- ),
- onPressed: onSettingsTap ?? () {
- // Open content preferences
- _showContentPreferences(context);
- },
+ icon: const Icon(Icons.tune, size: 24, color: Palette.icons),
+ onPressed:
+ onSettingsTap ??
+ () {
+ // Open content preferences
+ _showContentPreferences(context);
+ },
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
@@ -171,4 +167,3 @@ class ExploreNavBar extends StatelessWidget {
);
}
}
-
diff --git a/lib/features/explore/widgets/suggested_tweet_card.dart b/lib/features/explore/widgets/suggested_tweet_card.dart
index e1f724d..b218e45 100644
--- a/lib/features/explore/widgets/suggested_tweet_card.dart
+++ b/lib/features/explore/widgets/suggested_tweet_card.dart
@@ -1,16 +1,12 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import '../models/suggested_tweet_model.dart';
class SuggestedTweetCard extends StatelessWidget {
final SuggestedTweetModel tweet;
final VoidCallback? onTap;
- const SuggestedTweetCard({
- super.key,
- required this.tweet,
- this.onTap,
- });
+ const SuggestedTweetCard({super.key, required this.tweet, this.onTap});
String _formatCount(int count) {
if (count >= 1000000) {
@@ -24,19 +20,16 @@ class SuggestedTweetCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
- onTap: onTap ?? () {
- // Navigate to tweet detail
- },
+ onTap:
+ onTap ??
+ () {
+ // Navigate to tweet detail
+ },
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
- ),
+ border: Border(bottom: BorderSide(color: Palette.divider, width: 1)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -207,10 +200,7 @@ class SuggestedTweetCard extends StatelessWidget {
const SizedBox(width: 4),
Text(
_formatCount(count),
- style: const TextStyle(
- fontSize: 13,
- color: Palette.icons,
- ),
+ style: const TextStyle(fontSize: 13, color: Palette.icons),
),
],
),
@@ -218,4 +208,3 @@ class SuggestedTweetCard extends StatelessWidget {
);
}
}
-
diff --git a/lib/features/explore/widgets/trend_card.dart b/lib/features/explore/widgets/trend_card.dart
index 77228ad..c9e35f7 100644
--- a/lib/features/explore/widgets/trend_card.dart
+++ b/lib/features/explore/widgets/trend_card.dart
@@ -1,16 +1,12 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import '../models/trend_model.dart';
class TrendCard extends StatelessWidget {
final TrendModel trend;
final VoidCallback? onTap;
- const TrendCard({
- super.key,
- required this.trend,
- this.onTap,
- });
+ const TrendCard({super.key, required this.trend, this.onTap});
String _formatPostCount(int count) {
if (count >= 1000000) {
@@ -24,19 +20,16 @@ class TrendCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
- onTap: onTap ?? () {
- // Navigate to trend timeline
- },
+ onTap:
+ onTap ??
+ () {
+ // Navigate to trend timeline
+ },
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: Palette.divider,
- width: 1,
- ),
- ),
+ border: Border(bottom: BorderSide(color: Palette.divider, width: 1)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -55,8 +48,8 @@ class TrendCard extends StatelessWidget {
trend.location != null
? 'Trending in ${trend.location}'
: trend.category != null
- ? 'Trending in ${trend.category}'
- : '',
+ ? 'Trending in ${trend.category}'
+ : '',
style: const TextStyle(
fontSize: 13,
color: Palette.textSecondary,
@@ -124,4 +117,3 @@ class TrendCard extends StatelessWidget {
);
}
}
-
diff --git a/lib/features/explore/widgets/who_to_follow_card.dart b/lib/features/explore/widgets/who_to_follow_card.dart
index e1f152f..db2f0a1 100644
--- a/lib/features/explore/widgets/who_to_follow_card.dart
+++ b/lib/features/explore/widgets/who_to_follow_card.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import '../models/who_to_follow_model.dart';
class WhoToFollowCard extends StatelessWidget {
@@ -19,9 +19,11 @@ class WhoToFollowCard extends StatelessWidget {
return Material(
color: Colors.transparent,
child: InkWell(
- onTap: onTap ?? () {
- // Navigate to user profile
- },
+ onTap:
+ onTap ??
+ () {
+ // Navigate to user profile
+ },
borderRadius: BorderRadius.circular(12),
child: Ink(
decoration: BoxDecoration(
@@ -35,9 +37,11 @@ class WhoToFollowCard extends StatelessWidget {
children: [
// Avatar Section (Left)
GestureDetector(
- onTap: onTap ?? () {
- // Navigate to user profile
- },
+ onTap:
+ onTap ??
+ () {
+ // Navigate to user profile
+ },
child: CircleAvatar(
radius: 28,
backgroundColor: Palette.primary,
@@ -164,4 +168,3 @@ class WhoToFollowCard extends StatelessWidget {
);
}
}
-
diff --git a/lib/features/home/view/widgets/profile_side_drawer.dart b/lib/features/home/view/widgets/profile_side_drawer.dart
index 206e298..25fe257 100644
--- a/lib/features/home/view/widgets/profile_side_drawer.dart
+++ b/lib/features/home/view/widgets/profile_side_drawer.dart
@@ -3,6 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/models/usermodel.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
+import 'package:lite_x/core/providers/unseenChatsCountProvider.dart';
+import 'package:lite_x/core/view/screen/app_shell.dart';
+import 'package:lite_x/features/chat/repositories/socket_repository.dart';
+import 'package:lite_x/features/home/providers/user_profile_provider.dart';
import 'package:lite_x/features/profile/models/profile_model.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/profile/view_model/providers.dart';
@@ -77,10 +81,12 @@ class ProfileSideDrawer extends ConsumerWidget {
const SizedBox(height: 4),
Row(
children: [
- Text(
- '@${profileData?.username ?? user?.username ?? 'username'}',
- style: TextStyle(color: Colors.grey[500], fontSize: 14),
- overflow: TextOverflow.ellipsis,
+ Expanded(
+ child: Text(
+ '@${profileData?.username ?? user?.username ?? 'username'}',
+ style: TextStyle(color: Colors.grey[500], fontSize: 14),
+ overflow: TextOverflow.ellipsis,
+ ),
),
if (profileData?.isVerified == true) ...[
const SizedBox(width: 4),
@@ -132,9 +138,13 @@ class ProfileSideDrawer extends ConsumerWidget {
),
_DrawerItem(
icon: Icons.chat_bubble_outline,
- trailing: _buildBadge('Beta'),
label: 'Chat',
- onTap: () {},
+ onTap: () {
+ Navigator.pop(context);
+ ref.read(unseenChatsCountProvider.notifier).state = 0;
+ ref.read(socketRepositoryProvider).sendOpenMessageTab();
+ ref.read(shellNavigationProvider.notifier).state = 4;
+ },
),
_DrawerItem(
icon: Icons.bookmark_border,
diff --git a/lib/features/search/view/search_screen.dart b/lib/features/search/view/search_screen.dart
index d9d23b4..1405491 100644
--- a/lib/features/search/view/search_screen.dart
+++ b/lib/features/search/view/search_screen.dart
@@ -4,7 +4,8 @@ import '../widgets/search_bar.dart' as sb;
import '../widgets/search_results_list.dart';
import '../widgets/search_history_list.dart';
import '../view_model/search_view_model.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
+
class SearchScreen extends ConsumerWidget {
const SearchScreen({super.key});
@@ -15,23 +16,29 @@ class SearchScreen extends ConsumerWidget {
return Scaffold(
backgroundColor: Palette.background,
body: Column(
- children: [
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16,vertical: 8),
- child: const sb.SearchBar(),
- ),
- const SizedBox(height: 14),
- Expanded(
- child: state.isLoading
- ? const Center(child: CircularProgressIndicator())
- : state.results.isNotEmpty
- ? SearchResultsList(results: state.results)
- : state.history.isNotEmpty
- ? SearchHistoryList(history: state.history)
- : const Text('Try searching for people, lists, or keywords',style: TextStyle(color: Palette.textSecondary,fontSize: 16)),
- ),
- ],
- ),
+ children: [
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: const sb.SearchBar(),
+ ),
+ const SizedBox(height: 14),
+ Expanded(
+ child: state.isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : state.results.isNotEmpty
+ ? SearchResultsList(results: state.results)
+ : state.history.isNotEmpty
+ ? SearchHistoryList(history: state.history)
+ : const Text(
+ 'Try searching for people, lists, or keywords',
+ style: TextStyle(
+ color: Palette.textSecondary,
+ fontSize: 16,
+ ),
+ ),
+ ),
+ ],
+ ),
);
}
}
diff --git a/lib/features/search/widgets/search_bar.dart b/lib/features/search/widgets/search_bar.dart
index 3b86f2e..699e6d1 100644
--- a/lib/features/search/widgets/search_bar.dart
+++ b/lib/features/search/widgets/search_bar.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../view_model/search_view_model.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class SearchBar extends ConsumerWidget {
const SearchBar({super.key});
@@ -57,8 +57,10 @@ class SearchBar extends ConsumerWidget {
),
isDense: true,
- contentPadding:
- const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
+ contentPadding: const EdgeInsets.symmetric(
+ vertical: 0,
+ horizontal: 12,
+ ),
),
onChanged: (value) {
ref.read(searchViewModelProvider.notifier).search(value);
diff --git a/lib/features/search/widgets/search_results_list.dart b/lib/features/search/widgets/search_results_list.dart
index 9d06c4c..7402891 100644
--- a/lib/features/search/widgets/search_results_list.dart
+++ b/lib/features/search/widgets/search_results_list.dart
@@ -1,14 +1,11 @@
import 'package:flutter/material.dart';
import '../models/search_result_model.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class SearchResultsList extends StatelessWidget {
final List results;
- const SearchResultsList({
- super.key,
- required this.results,
- });
+ const SearchResultsList({super.key, required this.results});
@override
Widget build(BuildContext context) {
@@ -22,55 +19,60 @@ class SearchResultsList extends StatelessWidget {
final user = results[index];
return GestureDetector(
- onTap: () {},
- behavior: HitTestBehavior.opaque, // still responds to taps
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 12),
- child: Row(
- children: [
- CircleAvatar(
- backgroundImage: NetworkImage(user.avatarUrl ?? ''),
- radius: 20,
- ),
- const SizedBox(width: 12),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Row(
- children: [
- Flexible(
- child: Text(
- user.name,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- fontSize: 15,
- color: Palette.textWhite,
- fontWeight: FontWeight.bold,
+ onTap: () {},
+ behavior: HitTestBehavior.opaque, // still responds to taps
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ child: Row(
+ children: [
+ CircleAvatar(
+ backgroundImage: NetworkImage(user.avatarUrl ?? ''),
+ radius: 20,
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Row(
+ children: [
+ Flexible(
+ child: Text(
+ user.name,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(
+ fontSize: 15,
+ color: Palette.textWhite,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ if (user.isVerified) ...[
+ const SizedBox(width: 4),
+ const Icon(
+ Icons.check_circle,
+ size: 16,
+ color: Colors.blue,
+ ),
+ ],
+ ],
),
- ),
+ Text(
+ user.username,
+ style: const TextStyle(
+ fontSize: 15,
+ color: Colors.grey,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
),
- if (user.isVerified) ...[
- const SizedBox(width: 4),
- const Icon(Icons.check_circle,
- size: 16, color: Colors.blue),
- ],
- ],
- ),
- Text(
- user.username,
- style: const TextStyle(fontSize: 15, color: Colors.grey),
- overflow: TextOverflow.ellipsis,
- ),
- ],
+ ),
+ ],
+ ),
),
- ),
- ],
- ),
- ),
-);
-
+ );
},
);
}
diff --git a/lib/features/settings/screens/AccountInformation_Screen.dart b/lib/features/settings/screens/AccountInformation_Screen.dart
index 1a4199f..e2ed084 100644
--- a/lib/features/settings/screens/AccountInformation_Screen.dart
+++ b/lib/features/settings/screens/AccountInformation_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/profile/view_model/providers.dart';
diff --git a/lib/features/settings/screens/BlockedAccounts_Screen.dart b/lib/features/settings/screens/BlockedAccounts_Screen.dart
index d48b2ad..670d61b 100644
--- a/lib/features/settings/screens/BlockedAccounts_Screen.dart
+++ b/lib/features/settings/screens/BlockedAccounts_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/profile/models/user_model.dart';
import 'package:lite_x/features/settings/view_model/providers.dart';
diff --git a/lib/features/settings/screens/ChangePassword_Screen.dart b/lib/features/settings/screens/ChangePassword_Screen.dart
index 186ae66..49c2449 100644
--- a/lib/features/settings/screens/ChangePassword_Screen.dart
+++ b/lib/features/settings/screens/ChangePassword_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/profile/view_model/providers.dart';
diff --git a/lib/features/settings/screens/MuteAndBlock_Screen.dart b/lib/features/settings/screens/MuteAndBlock_Screen.dart
index ea73726..8053366 100644
--- a/lib/features/settings/screens/MuteAndBlock_Screen.dart
+++ b/lib/features/settings/screens/MuteAndBlock_Screen.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
diff --git a/lib/features/settings/screens/MutedAccounts_Screen.dart b/lib/features/settings/screens/MutedAccounts_Screen.dart
index 6ccb888..8d5d521 100644
--- a/lib/features/settings/screens/MutedAccounts_Screen.dart
+++ b/lib/features/settings/screens/MutedAccounts_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/profile/models/shared.dart';
import 'package:lite_x/features/profile/models/user_model.dart';
import 'package:lite_x/features/settings/view_model/providers.dart';
diff --git a/lib/features/settings/screens/PrivacyAndSafety_Screen.dart b/lib/features/settings/screens/PrivacyAndSafety_Screen.dart
index d45dae1..f7d29c4 100644
--- a/lib/features/settings/screens/PrivacyAndSafety_Screen.dart
+++ b/lib/features/settings/screens/PrivacyAndSafety_Screen.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
@@ -7,135 +7,237 @@ class PrivacyAndSafetyScreen extends StatelessWidget {
const PrivacyAndSafetyScreen({super.key});
Widget _sectionTitle(String title) => Padding(
- padding: const EdgeInsets.fromLTRB(16, 18, 16, 8),
- child: Text(title, style: const TextStyle(color: Palette.textWhite, fontSize: 20, fontWeight: FontWeight.w700)),
- );
+ padding: const EdgeInsets.fromLTRB(16, 18, 16, 8),
+ child: Text(
+ title,
+ style: const TextStyle(
+ color: Palette.textWhite,
+ fontSize: 20,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ );
- Widget _tile(IconData icon, String title, String subtitle, {VoidCallback? onTap}) => ListTile(
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- leading: SizedBox(
- width: 40,
- height: 40,
- child: Center(child: Icon(icon, color: Palette.textWhite, size: 18)),
- ),
- title: Text(title, style: const TextStyle(color: Palette.textWhite, fontWeight: FontWeight.w600)),
- subtitle: Text(subtitle, style: const TextStyle(color: Palette.textSecondary, fontSize: 13)),
- onTap: onTap ?? () {},
- );
+ Widget _tile(
+ IconData icon,
+ String title,
+ String subtitle, {
+ VoidCallback? onTap,
+ }) => ListTile(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ leading: SizedBox(
+ width: 40,
+ height: 40,
+ child: Center(child: Icon(icon, color: Palette.textWhite, size: 18)),
+ ),
+ title: Text(
+ title,
+ style: const TextStyle(
+ color: Palette.textWhite,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ subtitle: Text(
+ subtitle,
+ style: const TextStyle(color: Palette.textSecondary, fontSize: 13),
+ ),
+ onTap: onTap ?? () {},
+ );
Widget _linkTile(String title) => ListTile(
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
- title: Text(title, style: const TextStyle(color: Palette.textWhite)),
- onTap: () {},
- );
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ title: Text(title, style: const TextStyle(color: Palette.textWhite)),
+ onTap: () {},
+ );
Widget _buildContent(BuildContext context) => SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(height: 8),
- _sectionTitle('Your X activity'),
- _tile(Icons.group, 'Audience and tagging', 'Manage what information you allow other people on X to see.'),
- _tile(Icons.edit, 'Your posts', 'Manage the information associated with your posts.'),
- _tile(Icons.view_list, 'Content you see', 'Decide what you see on X based on your preferences.'),
- _tile(
- Icons.block,
- 'Mute and block',
- 'Manage the accounts, words, and notifications that you\'ve muted or blocked.',
- onTap: () => GoRouter.of(context).pushNamed(RouteConstants.muteandblockscreen),
- ),
- _tile(Icons.mail_outline, 'Direct messages', 'Manage who can message you directly.'),
- _tile(Icons.mic, 'Spaces', 'Manage your Spaces activity'),
- _tile(Icons.person_search, 'Discoverability and contacts', 'Control your discoverability settings and manage contacts you\'ve imported.'),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 8),
+ _sectionTitle('Your X activity'),
+ _tile(
+ Icons.group,
+ 'Audience and tagging',
+ 'Manage what information you allow other people on X to see.',
+ ),
+ _tile(
+ Icons.edit,
+ 'Your posts',
+ 'Manage the information associated with your posts.',
+ ),
+ _tile(
+ Icons.view_list,
+ 'Content you see',
+ 'Decide what you see on X based on your preferences.',
+ ),
+ _tile(
+ Icons.block,
+ 'Mute and block',
+ 'Manage the accounts, words, and notifications that you\'ve muted or blocked.',
+ onTap: () =>
+ GoRouter.of(context).pushNamed(RouteConstants.muteandblockscreen),
+ ),
+ _tile(
+ Icons.mail_outline,
+ 'Direct messages',
+ 'Manage who can message you directly.',
+ ),
+ _tile(Icons.mic, 'Spaces', 'Manage your Spaces activity'),
+ _tile(
+ Icons.person_search,
+ 'Discoverability and contacts',
+ 'Control your discoverability settings and manage contacts you\'ve imported.',
+ ),
- const SizedBox(height: 18),
- _sectionTitle('Data sharing and personalization'),
- _tile(Icons.open_in_new, 'Ads preferences', 'Manage your ads experience on X.'),
- _tile(Icons.show_chart, 'Inferred identity', 'Allow X to personalize your experience with your inferred activity.'),
- _tile(Icons.sync_alt, 'Data sharing with business partners', 'Allow sharing of additional information with X\'s business partners.'),
- _tile(Icons.location_on, 'Location information', 'Manage the location information X uses to personalize your experience.'),
- _tile(Icons.shield, 'Grok & Third-party Collaborators', 'Allow your public data and interactions to be used for training and fine-tuning.'),
+ const SizedBox(height: 18),
+ _sectionTitle('Data sharing and personalization'),
+ _tile(
+ Icons.open_in_new,
+ 'Ads preferences',
+ 'Manage your ads experience on X.',
+ ),
+ _tile(
+ Icons.show_chart,
+ 'Inferred identity',
+ 'Allow X to personalize your experience with your inferred activity.',
+ ),
+ _tile(
+ Icons.sync_alt,
+ 'Data sharing with business partners',
+ 'Allow sharing of additional information with X\'s business partners.',
+ ),
+ _tile(
+ Icons.location_on,
+ 'Location information',
+ 'Manage the location information X uses to personalize your experience.',
+ ),
+ _tile(
+ Icons.shield,
+ 'Grok & Third-party Collaborators',
+ 'Allow your public data and interactions to be used for training and fine-tuning.',
+ ),
- const SizedBox(height: 24),
- Container(
- width: double.infinity,
- color: Palette.cardBackground,
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
- child: const Text('Learn more about privacy on X', style: TextStyle(color: Palette.textWhite, fontWeight: FontWeight.w700, fontSize: 18)),
+ const SizedBox(height: 24),
+ Container(
+ width: double.infinity,
+ color: Palette.cardBackground,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
+ child: const Text(
+ 'Learn more about privacy on X',
+ style: TextStyle(
+ color: Palette.textWhite,
+ fontWeight: FontWeight.w700,
+ fontSize: 18,
),
- const SizedBox(height: 12),
- _linkTile('Privacy center'),
- _linkTile('Privacy policy'),
- _linkTile('Contact us'),
- const SizedBox(height: 48),
- ],
+ ),
),
- );
+ const SizedBox(height: 12),
+ _linkTile('Privacy center'),
+ _linkTile('Privacy policy'),
+ _linkTile('Contact us'),
+ const SizedBox(height: 48),
+ ],
+ ),
+ );
@override
Widget build(BuildContext context) {
- return LayoutBuilder(builder: (context, constraints) {
- // Match the Settings screen's web container size and mobile full-screen behavior
- if (constraints.maxWidth > 600) {
- return Scaffold(
- backgroundColor: Colors.black.withOpacity(0.4),
- body: Center(
- child: Container(
- width: 800,
- height: 700,
- decoration: BoxDecoration(
- color: Palette.background,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- children: [
- AppBar(
- backgroundColor: Palette.background,
- elevation: 0,
- leading: IconButton(
- icon: const Icon(Icons.arrow_back, color: Palette.textWhite, size: 20),
- onPressed: () => Navigator.of(context).pop(),
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ // Match the Settings screen's web container size and mobile full-screen behavior
+ if (constraints.maxWidth > 600) {
+ return Scaffold(
+ backgroundColor: Colors.black.withOpacity(0.4),
+ body: Center(
+ child: Container(
+ width: 800,
+ height: 700,
+ decoration: BoxDecoration(
+ color: Palette.background,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ children: [
+ AppBar(
+ backgroundColor: Palette.background,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(
+ Icons.arrow_back,
+ color: Palette.textWhite,
+ size: 20,
+ ),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: const [
+ Text(
+ 'Privacy and safety',
+ style: TextStyle(
+ color: Palette.textWhite,
+ fontSize: 18,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ SizedBox(height: 2),
+ Text(
+ '@profilename',
+ style: TextStyle(
+ color: Palette.textSecondary,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ centerTitle: false,
),
- title: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: const [
- Text('Privacy and safety', style: TextStyle(color: Palette.textWhite, fontSize: 18, fontWeight: FontWeight.w700)),
- SizedBox(height: 2),
- Text('@profilename', style: TextStyle(color: Palette.textSecondary, fontSize: 12)),
- ],
- ),
- centerTitle: false,
- ),
- Expanded(child: _buildContent(context)),
- ],
+ Expanded(child: _buildContent(context)),
+ ],
+ ),
),
),
- ),
- );
- }
+ );
+ }
- // Mobile layout (full screen)
- return Scaffold(
- backgroundColor: Palette.background,
- appBar: AppBar(
+ // Mobile layout (full screen)
+ return Scaffold(
backgroundColor: Palette.background,
- elevation: 0,
- leading: IconButton(
- icon: const Icon(Icons.arrow_back, color: Palette.textWhite, size: 20),
- onPressed: () => Navigator.of(context).pop(),
- ),
- title: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: const [
- Text('Privacy and safety', style: TextStyle(color: Palette.textWhite, fontSize: 18, fontWeight: FontWeight.w700)),
- SizedBox(height: 2),
- Text('@profilename', style: TextStyle(color: Palette.textSecondary, fontSize: 12)),
- ],
+ appBar: AppBar(
+ backgroundColor: Palette.background,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(
+ Icons.arrow_back,
+ color: Palette.textWhite,
+ size: 20,
+ ),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ title: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: const [
+ Text(
+ 'Privacy and safety',
+ style: TextStyle(
+ color: Palette.textWhite,
+ fontSize: 18,
+ fontWeight: FontWeight.w700,
+ ),
+ ),
+ SizedBox(height: 2),
+ Text(
+ '@profilename',
+ style: TextStyle(color: Palette.textSecondary, fontSize: 12),
+ ),
+ ],
+ ),
+ centerTitle: false,
),
- centerTitle: false,
- ),
- body: _buildContent(context),
- );
- });
+ body: _buildContent(context),
+ );
+ },
+ );
}
}
diff --git a/lib/features/settings/screens/SettingsAndPrivacy_Screen.dart b/lib/features/settings/screens/SettingsAndPrivacy_Screen.dart
index 43ca162..82c399e 100644
--- a/lib/features/settings/screens/SettingsAndPrivacy_Screen.dart
+++ b/lib/features/settings/screens/SettingsAndPrivacy_Screen.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:lite_x/features/settings/view/widgets/settings_search_bar.dart';
import 'package:lite_x/features/settings/view/widgets/settings_responsive_scaffold.dart';
@@ -91,7 +91,9 @@ class _SettingsList extends StatelessWidget {
Icon(LucideIcons.user, color: Palette.textWhite, size: 22),
'Your account',
'See information about your account, download an archive of your data, or learn about your account deactivation options.',
- onTap: () => GoRouter.of(context).pushNamed(RouteConstants.youraccountscreen),
+ onTap: () => GoRouter.of(
+ context,
+ ).pushNamed(RouteConstants.youraccountscreen),
),
_tile(
Icon(LucideIcons.lock, color: Palette.textWhite, size: 22),
diff --git a/lib/features/settings/screens/UserName_Screen.dart b/lib/features/settings/screens/UserName_Screen.dart
index 3f4d9c0..e9f2b17 100644
--- a/lib/features/settings/screens/UserName_Screen.dart
+++ b/lib/features/settings/screens/UserName_Screen.dart
@@ -4,7 +4,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/auth/view_model/auth_state.dart';
import 'package:lite_x/features/auth/view_model/auth_view_model.dart';
@@ -198,13 +198,13 @@ class _UsernameSettingsState extends ConsumerState {
onPressed: _isLoading ? null : _handleDone,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
- disabledBackgroundColor: Colors.blue.withOpacity(0.5),
+ disabledBackgroundColor: Colors.blue.withOpacity(0.4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
padding: const EdgeInsets.symmetric(
horizontal: 20,
- vertical: 2,
+ vertical: 4,
),
),
child: _isLoading
@@ -220,7 +220,7 @@ class _UsernameSettingsState extends ConsumerState {
'Done',
style: TextStyle(
color: Colors.white,
- fontSize: 14,
+ fontSize: 19,
fontWeight: FontWeight.w600,
),
),
diff --git a/lib/features/settings/screens/YourAccount_Screen.dart b/lib/features/settings/screens/YourAccount_Screen.dart
index 24b4c58..7fbd1d2 100644
--- a/lib/features/settings/screens/YourAccount_Screen.dart
+++ b/lib/features/settings/screens/YourAccount_Screen.dart
@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:lite_x/core/providers/current_user_provider.dart';
import 'package:lite_x/core/routes/Route_Constants.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
import 'package:lite_x/features/settings/view/widgets/settings_responsive_scaffold.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
@@ -16,16 +16,27 @@ class YourAccountScreen extends ConsumerWidget {
return username == null || username.isEmpty ? '' : '@$username';
}
- Widget _tile({required Widget leading, required String title, required String subtitle, VoidCallback? onTap}) => ListTile(
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- leading: SizedBox(width: 44, height: 44, child: Center(child: leading)),
- title: Text(
- title,
- style: const TextStyle(color: Palette.textWhite, fontWeight: FontWeight.w600),
- ),
- subtitle: Text(subtitle, style: const TextStyle(color: Palette.textSecondary, fontSize: 13)),
- onTap: onTap,
- );
+ Widget _tile({
+ required Widget leading,
+ required String title,
+ required String subtitle,
+ VoidCallback? onTap,
+ }) => ListTile(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ leading: SizedBox(width: 44, height: 44, child: Center(child: leading)),
+ title: Text(
+ title,
+ style: const TextStyle(
+ color: Palette.textWhite,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ subtitle: Text(
+ subtitle,
+ style: const TextStyle(color: Palette.textSecondary, fontSize: 13),
+ ),
+ onTap: onTap,
+ );
Widget _body(BuildContext context, String subtitle) {
return SingleChildScrollView(
@@ -37,36 +48,66 @@ class YourAccountScreen extends ConsumerWidget {
if (subtitle.isNotEmpty) ...[
Text(
subtitle,
- style: const TextStyle(color: Palette.textSecondary, fontSize: 16, fontWeight: FontWeight.w500),
+ style: const TextStyle(
+ color: Palette.textSecondary,
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ ),
),
const SizedBox(height: 8),
],
const SizedBox(height: 8),
const Text(
'See information about your account, download an archive of your data, or learn about account deactivation options.',
- style: TextStyle(color: Palette.textSecondary, fontSize: 16, height: 1.4),
+ style: TextStyle(
+ color: Palette.textSecondary,
+ fontSize: 16,
+ height: 1.4,
+ ),
),
const SizedBox(height: 24),
_tile(
- leading: Icon(LucideIcons.user, color: Palette.textWhite, size: 22),
+ leading: Icon(
+ LucideIcons.user,
+ color: Palette.textWhite,
+ size: 22,
+ ),
title: 'Account information',
- subtitle: 'See your account information like your phone number and email address.',
- onTap: () => GoRouter.of(context).pushNamed(RouteConstants.accountinformationscreen),
+ subtitle:
+ 'See your account information like your phone number and email address.',
+ onTap: () => GoRouter.of(
+ context,
+ ).pushNamed(RouteConstants.accountinformationscreen),
),
_tile(
- leading: Icon(LucideIcons.lock, color: Palette.textWhite, size: 22),
+ leading: Icon(
+ LucideIcons.lock,
+ color: Palette.textWhite,
+ size: 22,
+ ),
title: 'Change your password',
subtitle: 'Change your password at any time.',
- onTap: () => GoRouter.of(context).pushNamed(RouteConstants.changePasswordScreen),
+ onTap: () => GoRouter.of(
+ context,
+ ).pushNamed(RouteConstants.changePasswordScreen),
),
_tile(
- leading: Icon(LucideIcons.download, color: Palette.textWhite, size: 22),
+ leading: Icon(
+ LucideIcons.download,
+ color: Palette.textWhite,
+ size: 22,
+ ),
title: 'Download an archive of your data',
- subtitle: 'Get insights into the type of information stored for your account.',
+ subtitle:
+ 'Get insights into the type of information stored for your account.',
),
_tile(
- leading: Icon(LucideIcons.heart, color: Palette.textWhite, size: 22),
+ leading: Icon(
+ LucideIcons.heart,
+ color: Palette.textWhite,
+ size: 22,
+ ),
title: 'Deactivate Account',
subtitle: 'Find out how you can deactivate your account.',
),
@@ -79,20 +120,22 @@ class YourAccountScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
- return LayoutBuilder(builder: (context, constraints) {
- final subtitle = _subtitle(ref);
- if (constraints.maxWidth > 600) {
- return SettingsResponsiveScaffold.web(
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ final subtitle = _subtitle(ref);
+ if (constraints.maxWidth > 600) {
+ return SettingsResponsiveScaffold.web(
+ title: 'Your account',
+ subtitle: '@profilename',
+ body: _body(context, subtitle),
+ );
+ }
+ return SettingsResponsiveScaffold.mobile(
title: 'Your account',
- subtitle: '@profilename',
+ subtitle: '',
body: _body(context, subtitle),
);
- }
- return SettingsResponsiveScaffold.mobile(
- title: 'Your account',
- subtitle: '',
- body: _body(context, subtitle),
- );
- });
+ },
+ );
}
}
diff --git a/lib/features/settings/view/widgets/settings_responsive_scaffold.dart b/lib/features/settings/view/widgets/settings_responsive_scaffold.dart
index 2bf5bb9..43604c5 100644
--- a/lib/features/settings/view/widgets/settings_responsive_scaffold.dart
+++ b/lib/features/settings/view/widgets/settings_responsive_scaffold.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class SettingsResponsiveScaffold extends StatelessWidget {
final String title;
@@ -24,12 +24,12 @@ class SettingsResponsiveScaffold extends StatelessWidget {
required Widget body,
Widget? headerBottom,
}) => SettingsResponsiveScaffold._(
- title: title,
- subtitle: subtitle,
- body: body,
- headerBottom: headerBottom,
- isWeb: false,
- );
+ title: title,
+ subtitle: subtitle,
+ body: body,
+ headerBottom: headerBottom,
+ isWeb: false,
+ );
factory SettingsResponsiveScaffold.web({
required String title,
@@ -37,18 +37,22 @@ class SettingsResponsiveScaffold extends StatelessWidget {
required Widget body,
Widget? headerBottom,
}) => SettingsResponsiveScaffold._(
- title: title,
- subtitle: subtitle,
- body: body,
- headerBottom: headerBottom,
- isWeb: true,
- );
+ title: title,
+ subtitle: subtitle,
+ body: body,
+ headerBottom: headerBottom,
+ isWeb: true,
+ );
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
toolbarHeight: 56,
leading: IconButton(
- icon: const Icon(LucideIcons.arrowLeft, color: Palette.textWhite, size: 24),
+ icon: const Icon(
+ LucideIcons.arrowLeft,
+ color: Palette.textWhite,
+ size: 24,
+ ),
onPressed: () {
if (Navigator.of(context).canPop()) Navigator.of(context).pop();
},
@@ -68,7 +72,10 @@ class SettingsResponsiveScaffold extends StatelessWidget {
if (subtitle.isNotEmpty)
Text(
subtitle,
- style: const TextStyle(color: Palette.textSecondary, fontSize: 13),
+ style: const TextStyle(
+ color: Palette.textSecondary,
+ fontSize: 13,
+ ),
),
],
),
@@ -95,7 +102,10 @@ class SettingsResponsiveScaffold extends StatelessWidget {
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 8.0,
+ ),
child: body,
),
),
diff --git a/lib/features/settings/view/widgets/settings_search_bar.dart b/lib/features/settings/view/widgets/settings_search_bar.dart
index dbe18fb..8155370 100644
--- a/lib/features/settings/view/widgets/settings_search_bar.dart
+++ b/lib/features/settings/view/widgets/settings_search_bar.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
-import 'package:lite_x/core/theme/palette.dart';
+import 'package:lite_x/core/theme/Palette.dart';
class SettingsSearchBar extends StatelessWidget {
const SettingsSearchBar({super.key});
diff --git a/lib/main.dart b/lib/main.dart
index 31a99a7..7c9b381 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,6 +1,6 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_localizations/flutter_localizations.dart';
+// import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
@@ -58,12 +58,12 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
title: 'X Lite',
theme: appTheme,
- localizationsDelegates: const [
- GlobalMaterialLocalizations.delegate,
- GlobalWidgetsLocalizations.delegate,
- GlobalCupertinoLocalizations.delegate,
- ],
- supportedLocales: const [Locale('en'), Locale('ar')],
+ // localizationsDelegates: const [
+ // GlobalMaterialLocalizations.delegate,
+ // GlobalWidgetsLocalizations.delegate,
+ // GlobalCupertinoLocalizations.delegate,
+ // ],
+ // supportedLocales: const [Locale('en'), Locale('ar')],
routerConfig: Approuter.router,
);
}
diff --git a/test/.env b/test/.env
new file mode 100644
index 0000000..4b1f84b
--- /dev/null
+++ b/test/.env
@@ -0,0 +1 @@
+API_URL=https://example.com/
diff --git a/test/core/api_config_test.dart b/test/core/api_config_test.dart
new file mode 100644
index 0000000..2dd9b40
--- /dev/null
+++ b/test/core/api_config_test.dart
@@ -0,0 +1,31 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+import 'package:lite_x/core/constants/server_constants.dart';
+
+void main() async {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ setUpAll(() async {
+ dotenv.load(fileName: ".env");
+ });
+
+ group("API_test_URL", () {
+ test("should load API_test_URL from environment", () {
+ final apiUrl = dotenv.env["API_test_URL"];
+ expect(apiUrl, isNotNull);
+ expect(apiUrl, equals("https://example.com/"));
+ });
+ });
+
+ group("BASE_OPTIONS", () {
+ test("should set contentType to application/json", () {
+ expect(BASE_OPTIONS.contentType, equals("application/json"));
+ });
+
+ test("should have correct timeouts", () {
+ expect(BASE_OPTIONS.sendTimeout, equals(const Duration(seconds: 60)));
+ expect(BASE_OPTIONS.receiveTimeout, equals(const Duration(seconds: 60)));
+ expect(BASE_OPTIONS.connectTimeout, equals(const Duration(seconds: 60)));
+ });
+ });
+}
diff --git a/test/core/classes/picked_image_class_test.dart b/test/core/classes/picked_image_class_test.dart
new file mode 100644
index 0000000..b3f0bb7
--- /dev/null
+++ b/test/core/classes/picked_image_class_test.dart
@@ -0,0 +1,266 @@
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:lite_x/core/classes/PickedImage.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:image_picker/image_picker.dart';
+
+import 'picked_image_class_test.mocks.dart';
+
+@GenerateMocks([ImagePicker, XFile])
+void main() {
+ late MockImagePicker mockPicker;
+ late MockXFile mockXFile;
+
+ setUp(() {
+ mockPicker = MockImagePicker();
+ mockXFile = MockXFile();
+ });
+
+ group("pickImage", () {
+ test("should return PickedImage when image selected", () async {
+ when(mockXFile.path).thenReturn("/test/image.png");
+ when(mockXFile.name).thenReturn("image.png");
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenAnswer((_) async => mockXFile);
+
+ final result = await pickImage(picker: mockPicker);
+
+ expect(result, isA());
+ expect(result!.name, "image.png");
+ expect(result.path, "/test/image.png");
+ expect(result.file, isA());
+ verify(mockPicker.pickImage(source: anyNamed("source"))).called(1);
+ });
+
+ test("should return null when no image selected", () async {
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenAnswer((_) async => null);
+
+ final result = await pickImage(picker: mockPicker);
+
+ expect(result, null);
+ verify(mockPicker.pickImage(source: anyNamed("source"))).called(1);
+ });
+
+ test("should return null when picker throws exception", () async {
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenThrow(Exception("Picker error"));
+
+ final result = await pickImage(picker: mockPicker);
+
+ expect(result, null);
+ verify(mockPicker.pickImage(source: anyNamed("source"))).called(1);
+ });
+
+ test("should use default ImagePicker when picker is null", () async {
+ final result = await pickImage(picker: null);
+ expect(result, null);
+ });
+
+ test("should handle different exception types", () async {
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenThrow(ArgumentError("Invalid argument"));
+
+ final result = await pickImage(picker: mockPicker);
+
+ expect(result, null);
+ });
+ });
+
+ group("pickImages", () {
+ test("should return list with one PickedImage", () async {
+ when(mockXFile.path).thenReturn("/test/multi.png");
+ when(mockXFile.name).thenReturn("multi.png");
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenAnswer((_) async => mockXFile);
+
+ final result = await pickImages(maxImages: 4, picker: mockPicker);
+
+ expect(result.length, 1);
+ expect(result.first.name, "multi.png");
+ expect(result.first.path, "/test/multi.png");
+ expect(result.first.file, isA());
+ verify(mockPicker.pickImage(source: anyNamed("source"))).called(1);
+ });
+
+ test("should return empty list when no image selected", () async {
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenAnswer((_) async => null);
+
+ final result = await pickImages(maxImages: 4, picker: mockPicker);
+
+ expect(result, isEmpty);
+ verify(mockPicker.pickImage(source: anyNamed("source"))).called(1);
+ });
+
+ test("should return empty list when picker throws exception", () async {
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenThrow(Exception("Gallery access denied"));
+
+ final result = await pickImages(maxImages: 4, picker: mockPicker);
+
+ expect(result, isEmpty);
+ verify(mockPicker.pickImage(source: anyNamed("source"))).called(1);
+ });
+
+ test("should use default ImagePicker when picker is null", () async {
+ final result = await pickImages(maxImages: 4, picker: null);
+
+ expect(result, isEmpty);
+ });
+
+ test("should handle different maxImages parameter", () async {
+ when(mockXFile.path).thenReturn("/test/image.jpg");
+ when(mockXFile.name).thenReturn("image.jpg");
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenAnswer((_) async => mockXFile);
+
+ final result1 = await pickImages(maxImages: 1, picker: mockPicker);
+ final result2 = await pickImages(maxImages: 10, picker: mockPicker);
+
+ expect(result1.length, 1);
+ expect(result2.length, 1);
+ });
+
+ test("should handle different exception types", () async {
+ when(
+ mockPicker.pickImage(source: anyNamed("source")),
+ ).thenThrow(StateError("Invalid state"));
+
+ final result = await pickImages(maxImages: 4, picker: mockPicker);
+
+ expect(result, isEmpty);
+ });
+ });
+
+ group("pickVideo", () {
+ test("should return PickedImage when video selected", () async {
+ when(mockXFile.path).thenReturn("/test/video.mp4");
+ when(mockXFile.name).thenReturn("video.mp4");
+ when(
+ mockPicker.pickVideo(source: anyNamed("source")),
+ ).thenAnswer((_) async => mockXFile);
+
+ final result = await pickVideo(picker: mockPicker);
+
+ expect(result, isA());
+ expect(result!.name, "video.mp4");
+ expect(result.path, "/test/video.mp4");
+ expect(result.file, isA());
+ verify(mockPicker.pickVideo(source: anyNamed("source"))).called(1);
+ });
+
+ test("should return null when no video selected", () async {
+ when(
+ mockPicker.pickVideo(source: anyNamed("source")),
+ ).thenAnswer((_) async => null);
+
+ final result = await pickVideo(picker: mockPicker);
+
+ expect(result, null);
+ verify(mockPicker.pickVideo(source: anyNamed("source"))).called(1);
+ });
+
+ test("should return null when picker throws exception", () async {
+ when(
+ mockPicker.pickVideo(source: anyNamed("source")),
+ ).thenThrow(Exception("Video picker error"));
+
+ final result = await pickVideo(picker: mockPicker);
+
+ expect(result, null);
+ verify(mockPicker.pickVideo(source: anyNamed("source"))).called(1);
+ });
+
+ test("should use default ImagePicker when picker is null", () async {
+ final result = await pickVideo(picker: null);
+
+ expect(result, null);
+ });
+
+ test("should handle different exception types", () async {
+ when(
+ mockPicker.pickVideo(source: anyNamed("source")),
+ ).thenThrow(FormatException("Invalid format"));
+
+ final result = await pickVideo(picker: mockPicker);
+
+ expect(result, null);
+ });
+
+ test("should handle video with long path", () async {
+ final longPath = "/very/long/path/to/video/" * 10 + "video.mp4";
+ when(mockXFile.path).thenReturn(longPath);
+ when(mockXFile.name).thenReturn("video.mp4");
+ when(
+ mockPicker.pickVideo(source: anyNamed("source")),
+ ).thenAnswer((_) async => mockXFile);
+
+ final result = await pickVideo(picker: mockPicker);
+
+ expect(result, isNotNull);
+ expect(result!.path, longPath);
+ });
+ });
+
+ group("PickedImage class", () {
+ test("should create PickedImage with file", () {
+ final file = File("/test/image.png");
+ final pickedImage = PickedImage(
+ file: file,
+ name: "image.png",
+ path: "/test/image.png",
+ );
+
+ expect(pickedImage.file, equals(file));
+ expect(pickedImage.name, "image.png");
+ expect(pickedImage.path, "/test/image.png");
+ expect(pickedImage.bytes, null);
+ });
+
+ test("should create PickedImage with bytes", () {
+ final bytes = Uint8List.fromList([1, 2, 3, 4]);
+ final pickedImage = PickedImage(bytes: bytes, name: "image.png");
+
+ expect(pickedImage.bytes, equals(bytes));
+ expect(pickedImage.name, "image.png");
+ expect(pickedImage.file, null);
+ expect(pickedImage.path, null);
+ });
+
+ test("should create PickedImage with all parameters", () {
+ final file = File("/test/image.png");
+ final bytes = Uint8List.fromList([1, 2, 3, 4]);
+ final pickedImage = PickedImage(
+ file: file,
+ bytes: bytes,
+ name: "image.png",
+ path: "/test/image.png",
+ );
+
+ expect(pickedImage.file, equals(file));
+ expect(pickedImage.bytes, equals(bytes));
+ expect(pickedImage.name, "image.png");
+ expect(pickedImage.path, "/test/image.png");
+ });
+
+ test("should create PickedImage with only name", () {
+ final pickedImage = PickedImage(name: "image.png");
+
+ expect(pickedImage.name, "image.png");
+ expect(pickedImage.file, null);
+ expect(pickedImage.bytes, null);
+ expect(pickedImage.path, null);
+ });
+ });
+}
diff --git a/test/core/classes/picked_image_class_test.mocks.dart b/test/core/classes/picked_image_class_test.mocks.dart
new file mode 100644
index 0000000..320028d
--- /dev/null
+++ b/test/core/classes/picked_image_class_test.mocks.dart
@@ -0,0 +1,262 @@
+// Mocks generated by Mockito 5.4.6 from annotations
+// in lite_x/test/core/classes/picked_image_class_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i4;
+import 'dart:convert' as _i6;
+import 'dart:typed_data' as _i7;
+
+import 'package:image_picker/image_picker.dart' as _i3;
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'
+ as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:mockito/src/dummies.dart' as _i5;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: must_be_immutable
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeLostDataResponse_0 extends _i1.SmartFake
+ implements _i2.LostDataResponse {
+ _FakeLostDataResponse_0(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+class _FakeDateTime_1 extends _i1.SmartFake implements DateTime {
+ _FakeDateTime_1(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+/// A class which mocks [ImagePicker].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockImagePicker extends _i1.Mock implements _i3.ImagePicker {
+ MockImagePicker() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i4.Future<_i2.XFile?> pickImage({
+ required _i2.ImageSource? source,
+ double? maxWidth,
+ double? maxHeight,
+ int? imageQuality,
+ _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear,
+ bool? requestFullMetadata = true,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#pickImage, [], {
+ #source: source,
+ #maxWidth: maxWidth,
+ #maxHeight: maxHeight,
+ #imageQuality: imageQuality,
+ #preferredCameraDevice: preferredCameraDevice,
+ #requestFullMetadata: requestFullMetadata,
+ }),
+ returnValue: _i4.Future<_i2.XFile?>.value(),
+ )
+ as _i4.Future<_i2.XFile?>);
+
+ @override
+ _i4.Future> pickMultiImage({
+ double? maxWidth,
+ double? maxHeight,
+ int? imageQuality,
+ int? limit,
+ bool? requestFullMetadata = true,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#pickMultiImage, [], {
+ #maxWidth: maxWidth,
+ #maxHeight: maxHeight,
+ #imageQuality: imageQuality,
+ #limit: limit,
+ #requestFullMetadata: requestFullMetadata,
+ }),
+ returnValue: _i4.Future>.value(<_i2.XFile>[]),
+ )
+ as _i4.Future>);
+
+ @override
+ _i4.Future<_i2.XFile?> pickMedia({
+ double? maxWidth,
+ double? maxHeight,
+ int? imageQuality,
+ bool? requestFullMetadata = true,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#pickMedia, [], {
+ #maxWidth: maxWidth,
+ #maxHeight: maxHeight,
+ #imageQuality: imageQuality,
+ #requestFullMetadata: requestFullMetadata,
+ }),
+ returnValue: _i4.Future<_i2.XFile?>.value(),
+ )
+ as _i4.Future<_i2.XFile?>);
+
+ @override
+ _i4.Future> pickMultipleMedia({
+ double? maxWidth,
+ double? maxHeight,
+ int? imageQuality,
+ int? limit,
+ bool? requestFullMetadata = true,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#pickMultipleMedia, [], {
+ #maxWidth: maxWidth,
+ #maxHeight: maxHeight,
+ #imageQuality: imageQuality,
+ #limit: limit,
+ #requestFullMetadata: requestFullMetadata,
+ }),
+ returnValue: _i4.Future>.value(<_i2.XFile>[]),
+ )
+ as _i4.Future>);
+
+ @override
+ _i4.Future<_i2.XFile?> pickVideo({
+ required _i2.ImageSource? source,
+ _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear,
+ Duration? maxDuration,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#pickVideo, [], {
+ #source: source,
+ #preferredCameraDevice: preferredCameraDevice,
+ #maxDuration: maxDuration,
+ }),
+ returnValue: _i4.Future<_i2.XFile?>.value(),
+ )
+ as _i4.Future<_i2.XFile?>);
+
+ @override
+ _i4.Future> pickMultiVideo({
+ Duration? maxDuration,
+ int? limit,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#pickMultiVideo, [], {
+ #maxDuration: maxDuration,
+ #limit: limit,
+ }),
+ returnValue: _i4.Future>.value(<_i2.XFile>[]),
+ )
+ as _i4.Future>);
+
+ @override
+ _i4.Future<_i2.LostDataResponse> retrieveLostData() =>
+ (super.noSuchMethod(
+ Invocation.method(#retrieveLostData, []),
+ returnValue: _i4.Future<_i2.LostDataResponse>.value(
+ _FakeLostDataResponse_0(
+ this,
+ Invocation.method(#retrieveLostData, []),
+ ),
+ ),
+ )
+ as _i4.Future<_i2.LostDataResponse>);
+
+ @override
+ bool supportsImageSource(_i2.ImageSource? source) =>
+ (super.noSuchMethod(
+ Invocation.method(#supportsImageSource, [source]),
+ returnValue: false,
+ )
+ as bool);
+}
+
+/// A class which mocks [XFile].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockXFile extends _i1.Mock implements _i2.XFile {
+ MockXFile() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ String get path =>
+ (super.noSuchMethod(
+ Invocation.getter(#path),
+ returnValue: _i5.dummyValue(this, Invocation.getter(#path)),
+ )
+ as String);
+
+ @override
+ String get name =>
+ (super.noSuchMethod(
+ Invocation.getter(#name),
+ returnValue: _i5.dummyValue(this, Invocation.getter(#name)),
+ )
+ as String);
+
+ @override
+ _i4.Future saveTo(String? path) =>
+ (super.noSuchMethod(
+ Invocation.method(#saveTo, [path]),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future length() =>
+ (super.noSuchMethod(
+ Invocation.method(#length, []),
+ returnValue: _i4.Future.value(0),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future readAsString({
+ _i6.Encoding? encoding = const _i6.Utf8Codec(),
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(#readAsString, [], {#encoding: encoding}),
+ returnValue: _i4.Future.value(
+ _i5.dummyValue(
+ this,
+ Invocation.method(#readAsString, [], {#encoding: encoding}),
+ ),
+ ),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future<_i7.Uint8List> readAsBytes() =>
+ (super.noSuchMethod(
+ Invocation.method(#readAsBytes, []),
+ returnValue: _i4.Future<_i7.Uint8List>.value(_i7.Uint8List(0)),
+ )
+ as _i4.Future<_i7.Uint8List>);
+
+ @override
+ _i4.Stream<_i7.Uint8List> openRead([int? start, int? end]) =>
+ (super.noSuchMethod(
+ Invocation.method(#openRead, [start, end]),
+ returnValue: _i4.Stream<_i7.Uint8List>.empty(),
+ )
+ as _i4.Stream<_i7.Uint8List>);
+
+ @override
+ _i4.Future lastModified() =>
+ (super.noSuchMethod(
+ Invocation.method(#lastModified, []),
+ returnValue: _i4.Future.value(
+ _FakeDateTime_1(this, Invocation.method(#lastModified, [])),
+ ),
+ )
+ as _i4.Future);
+}
diff --git a/test/core/errors/app_failure_test.dart b/test/core/errors/app_failure_test.dart
new file mode 100644
index 0000000..d73c784
--- /dev/null
+++ b/test/core/errors/app_failure_test.dart
@@ -0,0 +1,51 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:lite_x/core/classes/AppFailure.dart';
+
+void main() {
+ group("AppFailure", () {
+ test("should store default message", () {
+ final failure = AppFailure();
+
+ expect(failure.message, "Unexpected error occurred");
+ });
+
+ test("should store provided message", () {
+ final failure = AppFailure(message: "Custom error");
+
+ expect(failure.message, "Custom error");
+ });
+
+ test("toString should return formatted string", () {
+ final failure = AppFailure(message: "Network issue");
+
+ expect(failure.toString(), "AppFailure(message: Network issue)");
+ });
+
+ test("should compare equal when messages match", () {
+ final f1 = AppFailure(message: "Error");
+ final f2 = AppFailure(message: "Error");
+
+ expect(f1, equals(f2));
+ });
+ test("identical objects should be equal", () {
+ final f1 = AppFailure(message: "Same");
+ final f2 = f1;
+
+ expect(f1 == f2, true);
+ });
+
+ test("should not be equal when messages differ", () {
+ final f1 = AppFailure(message: "Error1");
+ final f2 = AppFailure(message: "Error2");
+
+ expect(f1 == f2, false);
+ });
+
+ test("hashCode should match when messages match", () {
+ final f1 = AppFailure(message: "Hash");
+ final f2 = AppFailure(message: "Hash");
+
+ expect(f1.hashCode, f2.hashCode);
+ });
+ });
+}
diff --git a/test/features/auth/model/TokensModel_test.dart b/test/features/auth/model/TokensModel_test.dart
new file mode 100644
index 0000000..a4b0b39
--- /dev/null
+++ b/test/features/auth/model/TokensModel_test.dart
@@ -0,0 +1,127 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:lite_x/core/models/TokensModel.dart';
+
+void main() {
+ group('TokensModel Tests', () {
+ const String tAccessToken = 'access_token_value_longer_than_20_chars';
+ const String tRefreshToken = 'refresh_token_value_longer_than_20_chars';
+ test('fromMap creates instance from flat JSON map', () {
+ final map = {'accessToken': tAccessToken, 'refreshToken': tRefreshToken};
+
+ final model = TokensModel.fromMap(map);
+
+ expect(model.accessToken, tAccessToken);
+ expect(model.refreshToken, tRefreshToken);
+ expect(model.accessTokenExpiry.isAfter(DateTime.now()), true);
+ expect(model.refreshTokenExpiry.isAfter(DateTime.now()), true);
+ });
+ test('fromMap creates instance from nested "tokens" key', () {
+ final map = {
+ 'tokens': {'accessToken': tAccessToken, 'refreshToken': tRefreshToken},
+ };
+
+ final model = TokensModel.fromMap(map);
+
+ expect(model.accessToken, tAccessToken);
+ expect(model.refreshToken, tRefreshToken);
+ });
+ test('fromMap_login parses "Token" and "Refresh_token" correctly', () {
+ final map = {'Token': tAccessToken, 'Refresh_token': tRefreshToken};
+
+ final model = TokensModel.fromMap_login(map);
+
+ expect(model.accessToken, tAccessToken);
+ expect(model.refreshToken, tRefreshToken);
+ });
+
+ test('fromMap_reset_password parses "accesstoken" and "refresh_token"', () {
+ final map = {'accesstoken': tAccessToken, 'refresh_token': tRefreshToken};
+
+ final model = TokensModel.fromMap_reset_password(map);
+
+ expect(model.accessToken, tAccessToken);
+ expect(model.refreshToken, tRefreshToken);
+ });
+
+ test('fromMap_update parses "access" and "refresh"', () {
+ final map = {'access': tAccessToken, 'refresh': tRefreshToken};
+
+ final model = TokensModel.fromMap_update(map);
+
+ expect(model.accessToken, tAccessToken);
+ expect(model.refreshToken, tRefreshToken);
+ });
+
+ test('fromRefreshResponse parses "access_token" and "refresh_token"', () {
+ final map = {
+ 'access_token': tAccessToken,
+ 'refresh_token': tRefreshToken,
+ };
+
+ final model = TokensModel.fromRefreshResponse(map);
+
+ expect(model.accessToken, tAccessToken);
+ expect(model.refreshToken, tRefreshToken);
+ });
+
+ group('Expiry Logic', () {
+ test('should return TRUE if date is in the PAST', () {
+ final pastDate = DateTime.now().subtract(const Duration(days: 1));
+ final model = TokensModel(
+ accessToken: tAccessToken,
+ refreshToken: tRefreshToken,
+ accessTokenExpiry: pastDate,
+ refreshTokenExpiry: pastDate,
+ );
+
+ expect(model.isAccessTokenExpired, true);
+ expect(model.isRefreshTokenExpired, true);
+ });
+
+ test('should return FALSE if date is in the FUTURE', () {
+ final futureDate = DateTime.now().add(const Duration(days: 1));
+
+ final model = TokensModel(
+ accessToken: tAccessToken,
+ refreshToken: tRefreshToken,
+ accessTokenExpiry: futureDate,
+ refreshTokenExpiry: futureDate,
+ );
+
+ expect(model.isAccessTokenExpired, false);
+ expect(model.isRefreshTokenExpired, false);
+ });
+ });
+
+ test('toMap returns correct map structure', () {
+ final date = DateTime.now();
+ final model = TokensModel(
+ accessToken: tAccessToken,
+ refreshToken: tRefreshToken,
+ accessTokenExpiry: date,
+ refreshTokenExpiry: date,
+ );
+
+ final result = model.toMap();
+
+ expect(result['access_token'], tAccessToken);
+ expect(result['refresh_token'], tRefreshToken);
+ expect(result['access_token_expiry'], date.toIso8601String());
+ expect(result['refresh_token_expiry'], date.toIso8601String());
+ });
+
+ test('toString returns formatted string with substrings', () {
+ final model = TokensModel(
+ accessToken: '12345678901234567890_extra',
+ refreshToken: 'abcdefghijabcdefghij_extra',
+ accessTokenExpiry: DateTime(2025),
+ refreshTokenExpiry: DateTime(2026),
+ );
+
+ final str = model.toString();
+ expect(str.contains('12345678901234567890'), true);
+ expect(str.contains('abcdefghijabcdefghij'), true);
+ expect(str.contains('_extra'), false);
+ });
+ });
+}
diff --git a/test/features/auth/model/explore_category_test.dart b/test/features/auth/model/explore_category_test.dart
new file mode 100644
index 0000000..158f809
--- /dev/null
+++ b/test/features/auth/model/explore_category_test.dart
@@ -0,0 +1,42 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:lite_x/features/auth/models/ExploreCategory.dart';
+
+void main() {
+ group("ExploreCategory", () {
+ test("should create ExploreCategory with correct values", () {
+ final category = ExploreCategory(id: "1", name: "Tech");
+
+ expect(category.id, "1");
+ expect(category.name, "Tech");
+ });
+
+ test("fromMap should parse valid map correctly", () {
+ final map = {"id": "10", "name": "Business"};
+
+ final category = ExploreCategory.fromMap(map);
+
+ expect(category.id, "10");
+ expect(category.name, "Business");
+ });
+
+ test("fromMap should throw error if id is missing", () {
+ final map = {"name": "Sports"};
+
+ expect(() => ExploreCategory.fromMap(map), throwsA(isA()));
+ });
+
+ test("fromMap should throw error if name is missing", () {
+ final map = {"id": "55"};
+
+ expect(() => ExploreCategory.fromMap(map), throwsA(isA()));
+ });
+
+ test("two ExploreCategory objects with same values should be equal", () {
+ final c1 = ExploreCategory(id: "1", name: "Tech");
+ final c2 = ExploreCategory(id: "1", name: "Tech");
+
+ expect(c1.id, c2.id);
+ expect(c1.name, c2.name);
+ });
+ });
+}
diff --git a/test/features/auth/model/usermodel_test.dart b/test/features/auth/model/usermodel_test.dart
new file mode 100644
index 0000000..c014c2a
--- /dev/null
+++ b/test/features/auth/model/usermodel_test.dart
@@ -0,0 +1,161 @@
+import 'dart:convert';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:lite_x/core/models/usermodel.dart';
+
+void main() {
+ group('UserModel Tests', () {
+ final tUser = UserModel(
+ name: 'Test User',
+ email: 'test@example.com',
+ dob: '2000-01-01',
+ username: 'testuser',
+ id: '123',
+ isEmailVerified: true,
+ isVerified: true,
+ tfaVerified: true,
+ interests: {'Coding', 'Flutter'},
+ photo: 'photo_id',
+ bio: 'Hello world',
+ );
+
+ test('equality fails when specific fields differ', () {
+ expect(tUser == tUser.copyWith(name: 'Diff'), isFalse);
+ expect(tUser == tUser.copyWith(email: 'diff@mail.com'), isFalse);
+ expect(tUser == tUser.copyWith(dob: '1900-01-01'), isFalse);
+ expect(tUser == tUser.copyWith(username: 'diff_user'), isFalse);
+ expect(tUser == tUser.copyWith(photo: 'diff_photo'), isFalse);
+ expect(tUser == tUser.copyWith(bio: 'Diff bio'), isFalse);
+ expect(tUser == tUser.copyWith(id: '999'), isFalse);
+ expect(
+ tUser == tUser.copyWith(isEmailVerified: !tUser.isEmailVerified),
+ isFalse,
+ );
+ expect(tUser == tUser.copyWith(isVerified: !tUser.isVerified), isFalse);
+ expect(
+ tUser == tUser.copyWith(tfaVerified: !(tUser.tfaVerified ?? false)),
+ isFalse,
+ );
+ expect(tUser == tUser.copyWith(interests: {'Other'}), isFalse);
+ });
+
+ test('hashCode changes when fields change', () {
+ final tUserDiff = tUser.copyWith(name: 'Different Name');
+ expect(tUser.hashCode, isNot(equals(tUserDiff.hashCode)));
+ });
+
+ test('copyWith creates a new instance with updated values', () {
+ final result = tUser.copyWith(
+ name: 'New Name',
+ interests: {'Dart'},
+ localProfilePhotoPath: '/path/to/image',
+ );
+
+ expect(result.name, 'New Name');
+ expect(result.interests, {'Dart'});
+ expect(result.localProfilePhotoPath, '/path/to/image');
+ expect(result.email, tUser.email);
+ });
+
+ test('fromMap returns correct UserModel with valid map', () {
+ final map = {
+ 'name': 'Test User',
+ 'email': 'test@example.com',
+ 'dateOfBirth': '2000-01-01',
+ 'username': 'testuser',
+ 'id': 123,
+ 'isEmailVerified': true,
+ 'isVerified': true,
+ 'tfaVerifed': true,
+ 'interests': ['Coding', 'Flutter'],
+ 'photo': 'photo_id',
+ 'bio': 'Hello world',
+ };
+
+ final result = UserModel.fromMap(map);
+ expect(result, tUser);
+ });
+
+ test('fromMap handles fallback to "dob" key', () {
+ final map = {
+ 'name': 'Test User',
+ 'email': 'test@example.com',
+ 'dob': '1999-12-31',
+ 'username': 'testuser',
+ 'id': '123',
+ 'isEmailVerified': false,
+ 'isVerified': false,
+ };
+
+ final result = UserModel.fromMap(map);
+ expect(result.dob, '1999-12-31');
+ });
+
+ test('fromMap parses interests from Set input', () {
+ final map = {
+ 'name': 'Test User',
+ 'email': 'test@example.com',
+ 'dob': '2000-01-01',
+ 'username': 'testuser',
+ 'id': '123',
+ 'interests': {'Tech', 'Design'},
+ };
+
+ final result = UserModel.fromMap(map);
+ expect(result.interests, {'Tech', 'Design'});
+ });
+
+ test('fromMap handles null interests', () {
+ final map = {
+ 'name': 'Test User',
+ 'email': 'test@example.com',
+ 'dob': '2000-01-01',
+ 'username': 'testuser',
+ 'id': '123',
+ 'interests': null,
+ };
+
+ final result = UserModel.fromMap(map);
+ expect(result.interests, isEmpty);
+ });
+
+ test('toMap returns correct map', () {
+ final result = tUser.toMap();
+ expect(result['name'], 'Test User');
+ expect(result['email'], 'test@example.com');
+ expect(result['dateOfBirth'], '2000-01-01');
+ expect(result['tfaVerifed'], true);
+ expect(result['interests'], ['Coding', 'Flutter']);
+ });
+
+ test('toJson returns correct json string', () {
+ final result = tUser.toJson();
+ final decoded = json.decode(result);
+ expect(decoded['username'], 'testuser');
+ expect(decoded['id'], '123');
+ });
+
+ test('fromJson returns correct UserModel', () {
+ final jsonStr = json.encode({
+ 'name': 'Test User',
+ 'email': 'test@example.com',
+ 'dateOfBirth': '2000-01-01',
+ 'username': 'testuser',
+ 'id': '123',
+ 'isEmailVerified': true,
+ 'isVerified': true,
+ 'tfaVerifed': true,
+ 'interests': ['Coding', 'Flutter'],
+ 'photo': 'photo_id',
+ 'bio': 'Hello world',
+ });
+
+ final result = UserModel.fromJson(jsonStr);
+ expect(result, tUser);
+ });
+
+ test('toString contains correct class name', () {
+ expect(tUser.toString(), contains('UserModel'));
+ expect(tUser.toString(), contains('test@example.com'));
+ });
+ });
+}
diff --git a/test/features/auth/repositories/auth_local_repository_test.dart b/test/features/auth/repositories/auth_local_repository_test.dart
new file mode 100644
index 0000000..539e6fa
--- /dev/null
+++ b/test/features/auth/repositories/auth_local_repository_test.dart
@@ -0,0 +1,378 @@
+import 'dart:async';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:hive_ce/hive.dart';
+import 'package:lite_x/core/models/TokensModel.dart';
+import 'package:lite_x/core/models/usermodel.dart';
+import 'package:lite_x/features/auth/repositories/auth_local_repository.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+import 'auth_local_repository_test.mocks.dart';
+
+@GenerateMocks([Box])
+void main() {
+ late MockBox mockUserBox;
+ late MockBox mockTokenBox;
+ late AuthLocalRepository repository;
+
+ setUp(() {
+ mockUserBox = MockBox();
+ mockTokenBox = MockBox();
+ repository = AuthLocalRepository(
+ userBox: mockUserBox,
+ tokenBox: mockTokenBox,
+ );
+ });
+
+ tearDown(() {
+ repository.dispose();
+ });
+
+ group('User Management', () {
+ final testUser = UserModel(
+ name: 'Test User',
+ email: 'test@example.com',
+ dob: '1990-01-01',
+ username: 'testuser',
+ id: '123',
+ isEmailVerified: true,
+ isVerified: false,
+ bio: 'Test bio',
+ photo: 'photo123',
+ tfaVerified: true,
+ interests: {'sports', 'music'},
+ localProfilePhotoPath: '/path/to/photo',
+ );
+
+ test('saveUser should store user in box', () async {
+ when(
+ mockUserBox.put('currentUser', testUser),
+ ).thenAnswer((_) async => Future.value());
+
+ await repository.saveUser(testUser);
+
+ verify(mockUserBox.put('currentUser', testUser)).called(1);
+ });
+
+ test('getUser should return user from box', () {
+ when(mockUserBox.get('currentUser')).thenReturn(testUser);
+
+ final result = repository.getUser();
+
+ expect(result, equals(testUser));
+ verify(mockUserBox.get('currentUser')).called(1);
+ });
+
+ test('getUser should return null when no user exists', () {
+ when(mockUserBox.get('currentUser')).thenReturn(null);
+
+ final result = repository.getUser();
+
+ expect(result, isNull);
+ verify(mockUserBox.get('currentUser')).called(1);
+ });
+
+ test('clearUser should delete user from box', () async {
+ when(
+ mockUserBox.delete('currentUser'),
+ ).thenAnswer((_) async => Future.value());
+
+ await repository.clearUser();
+
+ verify(mockUserBox.delete('currentUser')).called(1);
+ });
+ });
+
+ group('Token Management', () {
+ final testTokens = TokensModel(
+ accessToken: 'access_token_123',
+ refreshToken: 'refresh_token_456',
+ accessTokenExpiry: DateTime(2025, 12, 31),
+ refreshTokenExpiry: DateTime(2026, 1, 31),
+ );
+
+ test('saveTokens should store all token data', () async {
+ when(mockTokenBox.putAll(any)).thenAnswer((_) async => Future.value());
+
+ await repository.saveTokens(testTokens);
+
+ final captured = verify(mockTokenBox.putAll(captureAny)).captured.single;
+ expect(captured['accessToken'], equals('access_token_123'));
+ expect(captured['refreshToken'], equals('refresh_token_456'));
+ expect(captured['accessTokenExpiry'], equals('2025-12-31T00:00:00.000'));
+ expect(captured['refreshTokenExpiry'], equals('2026-01-31T00:00:00.000'));
+ });
+
+ test('saveTokens should emit tokens to stream', () async {
+ when(mockTokenBox.putAll(any)).thenAnswer((_) async => Future.value());
+
+ expectLater(
+ repository.tokenStream,
+ emits(
+ predicate(
+ (t) =>
+ t.accessToken == testTokens.accessToken &&
+ t.refreshToken == testTokens.refreshToken,
+ ),
+ ),
+ );
+
+ await repository.saveTokens(testTokens);
+ });
+
+ test('getTokens should return TokensModel when all data exists', () {
+ when(mockTokenBox.get('accessToken')).thenReturn('access_token_123');
+ when(mockTokenBox.get('refreshToken')).thenReturn('refresh_token_456');
+ when(
+ mockTokenBox.get('accessTokenExpiry'),
+ ).thenReturn('2025-12-31T00:00:00.000');
+ when(
+ mockTokenBox.get('refreshTokenExpiry'),
+ ).thenReturn('2026-01-31T00:00:00.000');
+
+ final result = repository.getTokens();
+
+ expect(result, isNotNull);
+ expect(result!.accessToken, equals('access_token_123'));
+ expect(result.refreshToken, equals('refresh_token_456'));
+ expect(result.accessTokenExpiry, equals(DateTime(2025, 12, 31)));
+ expect(result.refreshTokenExpiry, equals(DateTime(2026, 1, 31)));
+ });
+
+ test('getTokens should return null when accessToken is missing', () {
+ when(mockTokenBox.get('accessToken')).thenReturn(null);
+ when(mockTokenBox.get('refreshToken')).thenReturn('refresh_token_456');
+ when(
+ mockTokenBox.get('accessTokenExpiry'),
+ ).thenReturn('2025-12-31T00:00:00.000');
+ when(
+ mockTokenBox.get('refreshTokenExpiry'),
+ ).thenReturn('2026-01-31T00:00:00.000');
+
+ final result = repository.getTokens();
+
+ expect(result, isNull);
+ });
+
+ test('getTokens should return null when refreshToken is missing', () {
+ when(mockTokenBox.get('accessToken')).thenReturn('access_token_123');
+ when(mockTokenBox.get('refreshToken')).thenReturn(null);
+ when(
+ mockTokenBox.get('accessTokenExpiry'),
+ ).thenReturn('2025-12-31T00:00:00.000');
+ when(
+ mockTokenBox.get('refreshTokenExpiry'),
+ ).thenReturn('2026-01-31T00:00:00.000');
+
+ final result = repository.getTokens();
+
+ expect(result, isNull);
+ });
+
+ test('getTokens should return null when accessTokenExpiry is missing', () {
+ when(mockTokenBox.get('accessToken')).thenReturn('access_token_123');
+ when(mockTokenBox.get('refreshToken')).thenReturn('refresh_token_456');
+ when(mockTokenBox.get('accessTokenExpiry')).thenReturn(null);
+ when(
+ mockTokenBox.get('refreshTokenExpiry'),
+ ).thenReturn('2026-01-31T00:00:00.000');
+
+ final result = repository.getTokens();
+
+ expect(result, isNull);
+ });
+
+ test('getTokens should return null when refreshTokenExpiry is missing', () {
+ when(mockTokenBox.get('accessToken')).thenReturn('access_token_123');
+ when(mockTokenBox.get('refreshToken')).thenReturn('refresh_token_456');
+ when(
+ mockTokenBox.get('accessTokenExpiry'),
+ ).thenReturn('2025-12-31T00:00:00.000');
+ when(mockTokenBox.get('refreshTokenExpiry')).thenReturn(null);
+
+ final result = repository.getTokens();
+
+ expect(result, isNull);
+ });
+
+ test('getTokens should return null when date parsing fails', () {
+ when(mockTokenBox.get('accessToken')).thenReturn('access_token_123');
+ when(mockTokenBox.get('refreshToken')).thenReturn('refresh_token_456');
+ when(mockTokenBox.get('accessTokenExpiry')).thenReturn('invalid_date');
+ when(
+ mockTokenBox.get('refreshTokenExpiry'),
+ ).thenReturn('2026-01-31T00:00:00.000');
+
+ final result = repository.getTokens();
+
+ expect(result, isNull);
+ });
+
+ test('clearTokens should delete all token data', () async {
+ when(
+ mockTokenBox.deleteAll(any),
+ ).thenAnswer((_) async => Future.value(4));
+
+ await repository.clearTokens();
+
+ final captured = verify(
+ mockTokenBox.deleteAll(captureAny),
+ ).captured.single;
+ expect(
+ captured,
+ containsAll([
+ 'accessToken',
+ 'refreshToken',
+ 'accessTokenExpiry',
+ 'refreshTokenExpiry',
+ ]),
+ );
+ });
+
+ test('clearTokens should emit null to stream', () async {
+ when(
+ mockTokenBox.deleteAll(any),
+ ).thenAnswer((_) async => Future.value(4));
+
+ expectLater(repository.tokenStream, emits(null));
+
+ await repository.clearTokens();
+ });
+ });
+
+ group('Token Stream', () {
+ test('tokenStream should emit multiple token updates', () async {
+ when(mockTokenBox.putAll(any)).thenAnswer((_) async => Future.value());
+
+ final tokens1 = TokensModel(
+ accessToken: 'token1',
+ refreshToken: 'refresh1',
+ accessTokenExpiry: DateTime(2025, 12, 31),
+ refreshTokenExpiry: DateTime(2026, 1, 31),
+ );
+
+ final tokens2 = TokensModel(
+ accessToken: 'token2',
+ refreshToken: 'refresh2',
+ accessTokenExpiry: DateTime(2025, 12, 31),
+ refreshTokenExpiry: DateTime(2026, 1, 31),
+ );
+
+ expectLater(
+ repository.tokenStream,
+ emitsInOrder([
+ predicate(
+ (t) =>
+ t.accessToken == tokens1.accessToken &&
+ t.refreshToken == tokens1.refreshToken,
+ ),
+ predicate(
+ (t) =>
+ t.accessToken == tokens2.accessToken &&
+ t.refreshToken == tokens2.refreshToken,
+ ),
+ ]),
+ );
+
+ await repository.saveTokens(tokens1);
+ await repository.saveTokens(tokens2);
+ });
+
+ test('tokenStream should emit token then null on clear', () async {
+ when(mockTokenBox.putAll(any)).thenAnswer((_) async => Future.value());
+ when(
+ mockTokenBox.deleteAll(any),
+ ).thenAnswer((_) async => Future.value(4));
+
+ final tokens = TokensModel(
+ accessToken: 'token',
+ refreshToken: 'refresh',
+ accessTokenExpiry: DateTime(2025, 12, 31),
+ refreshTokenExpiry: DateTime(2026, 1, 31),
+ );
+
+ expectLater(
+ repository.tokenStream,
+ emitsInOrder([
+ predicate(
+ (t) =>
+ t.accessToken == tokens.accessToken &&
+ t.refreshToken == tokens.refreshToken,
+ ),
+ isNull,
+ ]),
+ );
+
+ await repository.saveTokens(tokens);
+ await repository.clearTokens();
+ });
+
+ test('dispose should close token stream', () {
+ repository.dispose();
+
+ final sub = repository.tokenStream.listen((_) {});
+ expect(sub, isA());
+ expect(sub.isPaused, isFalse);
+ });
+ });
+
+ group('Integration Scenarios', () {
+ test('complete auth flow: save user and tokens, then retrieve', () async {
+ final user = UserModel(
+ name: 'John Doe',
+ email: 'john@example.com',
+ dob: '1995-05-15',
+ username: 'johndoe',
+ id: '456',
+ isEmailVerified: true,
+ isVerified: true,
+ );
+
+ final tokens = TokensModel(
+ accessToken: 'jwt_token',
+ refreshToken: 'refresh_jwt',
+ accessTokenExpiry: DateTime(2025, 12, 31),
+ refreshTokenExpiry: DateTime(2026, 1, 31),
+ );
+
+ when(
+ mockUserBox.put('currentUser', user),
+ ).thenAnswer((_) async => Future.value());
+ when(mockTokenBox.putAll(any)).thenAnswer((_) async => Future.value());
+ when(mockUserBox.get('currentUser')).thenReturn(user);
+ when(mockTokenBox.get('accessToken')).thenReturn('jwt_token');
+ when(mockTokenBox.get('refreshToken')).thenReturn('refresh_jwt');
+ when(
+ mockTokenBox.get('accessTokenExpiry'),
+ ).thenReturn('2025-12-31T00:00:00.000');
+ when(
+ mockTokenBox.get('refreshTokenExpiry'),
+ ).thenReturn('2026-01-31T00:00:00.000');
+
+ await repository.saveUser(user);
+ await repository.saveTokens(tokens);
+
+ final retrievedUser = repository.getUser();
+ final retrievedTokens = repository.getTokens();
+
+ expect(retrievedUser, equals(user));
+ expect(retrievedTokens?.accessToken, equals('jwt_token'));
+ expect(retrievedTokens?.refreshToken, equals('refresh_jwt'));
+ });
+
+ test('complete logout flow: clear user and tokens', () async {
+ when(
+ mockUserBox.delete('currentUser'),
+ ).thenAnswer((_) async => Future.value());
+ when(
+ mockTokenBox.deleteAll(any),
+ ).thenAnswer((_) async => Future.value(4));
+
+ await repository.clearUser();
+ await repository.clearTokens();
+
+ verify(mockUserBox.delete('currentUser')).called(1);
+ verify(mockTokenBox.deleteAll(any)).called(1);
+ });
+ });
+}
diff --git a/test/features/auth/repositories/auth_local_repository_test.mocks.dart b/test/features/auth/repositories/auth_local_repository_test.mocks.dart
new file mode 100644
index 0000000..5105610
--- /dev/null
+++ b/test/features/auth/repositories/auth_local_repository_test.mocks.dart
@@ -0,0 +1,233 @@
+// Mocks generated by Mockito 5.4.6 from annotations
+// in lite_x/test/features/auth/repositories/auth_local_repository_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i4;
+
+import 'package:hive_ce/src/box/box.dart' as _i2;
+import 'package:hive_ce/src/box/box_base.dart' as _i5;
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:mockito/src/dummies.dart' as _i3;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: must_be_immutable
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+/// A class which mocks [Box].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBox extends _i1.Mock implements _i2.Box {
+ MockBox() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ Iterable get values =>
+ (super.noSuchMethod(Invocation.getter(#values), returnValue: [])
+ as Iterable);
+
+ @override
+ String get name =>
+ (super.noSuchMethod(
+ Invocation.getter(#name),
+ returnValue: _i3.dummyValue(this, Invocation.getter(#name)),
+ )
+ as String);
+
+ @override
+ bool get isOpen =>
+ (super.noSuchMethod(Invocation.getter(#isOpen), returnValue: false)
+ as bool);
+
+ @override
+ bool get lazy =>
+ (super.noSuchMethod(Invocation.getter(#lazy), returnValue: false)
+ as bool);
+
+ @override
+ Iterable get keys =>
+ (super.noSuchMethod(Invocation.getter(#keys), returnValue: [])
+ as Iterable);
+
+ @override
+ int get length =>
+ (super.noSuchMethod(Invocation.getter(#length), returnValue: 0) as int);
+
+ @override
+ bool get isEmpty =>
+ (super.noSuchMethod(Invocation.getter(#isEmpty), returnValue: false)
+ as bool);
+
+ @override
+ bool get isNotEmpty =>
+ (super.noSuchMethod(Invocation.getter(#isNotEmpty), returnValue: false)
+ as bool);
+
+ @override
+ Iterable valuesBetween({dynamic startKey, dynamic endKey}) =>
+ (super.noSuchMethod(
+ Invocation.method(#valuesBetween, [], {
+ #startKey: startKey,
+ #endKey: endKey,
+ }),
+ returnValue: [],
+ )
+ as Iterable);
+
+ @override
+ E? getAt(int? index) =>
+ (super.noSuchMethod(Invocation.method(#getAt, [index])) as E?);
+
+ @override
+ Map toMap() =>
+ (super.noSuchMethod(
+ Invocation.method(#toMap, []),
+ returnValue: {},
+ )
+ as Map);
+
+ @override
+ dynamic keyAt(int? index) =>
+ super.noSuchMethod(Invocation.method(#keyAt, [index]));
+
+ @override
+ _i4.Stream<_i5.BoxEvent> watch({dynamic key}) =>
+ (super.noSuchMethod(
+ Invocation.method(#watch, [], {#key: key}),
+ returnValue: _i4.Stream<_i5.BoxEvent>.empty(),
+ )
+ as _i4.Stream<_i5.BoxEvent>);
+
+ @override
+ bool containsKey(dynamic key) =>
+ (super.noSuchMethod(
+ Invocation.method(#containsKey, [key]),
+ returnValue: false,
+ )
+ as bool);
+
+ @override
+ _i4.Future put(dynamic key, E? value) =>
+ (super.noSuchMethod(
+ Invocation.method(#put, [key, value]),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future putAt(int? index, E? value) =>
+ (super.noSuchMethod(
+ Invocation.method(#putAt, [index, value]),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future putAll(Map? entries) =>
+ (super.noSuchMethod(
+ Invocation.method(#putAll, [entries]),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future add(E? value) =>
+ (super.noSuchMethod(
+ Invocation.method(#add, [value]),
+ returnValue: _i4.Future.value(0),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future> addAll(Iterable? values) =>
+ (super.noSuchMethod(
+ Invocation.method(#addAll, [values]),
+ returnValue: _i4.Future>.value([]),
+ )
+ as _i4.Future>);
+
+ @override
+ _i4.Future delete(dynamic key) =>
+ (super.noSuchMethod(
+ Invocation.method(#delete, [key]),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future deleteAt(int? index) =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteAt, [index]),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future deleteAll(Iterable? keys) =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteAll, [keys]),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future compact() =>
+ (super.noSuchMethod(
+ Invocation.method(#compact, []),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future clear() =>
+ (super.noSuchMethod(
+ Invocation.method(#clear, []),
+ returnValue: _i4.Future.value(0),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future close() =>
+ (super.noSuchMethod(
+ Invocation.method(#close, []),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future deleteFromDisk() =>
+ (super.noSuchMethod(
+ Invocation.method(#deleteFromDisk, []),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+
+ @override
+ _i4.Future flush() =>
+ (super.noSuchMethod(
+ Invocation.method(#flush, []),
+ returnValue: _i4.Future.value(),
+ returnValueForMissingStub: _i4.Future.value(),
+ )
+ as _i4.Future);
+}
diff --git a/test/features/auth/repositories/auth_remote_repository_test.dart b/test/features/auth/repositories/auth_remote_repository_test.dart
new file mode 100644
index 0000000..3840627
--- /dev/null
+++ b/test/features/auth/repositories/auth_remote_repository_test.dart
@@ -0,0 +1,1545 @@
+// auth_remote_repository_test.dart
+import 'dart:io';
+import 'package:dio/dio.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:lite_x/core/classes/PickedImage.dart';
+import 'package:lite_x/features/auth/repositories/auth_remote_repository.dart';
+import 'package:mockito/annotations.dart';
+import 'package:lite_x/core/classes/AppFailure.dart';
+import 'package:lite_x/core/models/TokensModel.dart';
+import 'package:lite_x/core/models/usermodel.dart';
+import 'package:mockito/mockito.dart';
+import 'auth_remote_repository_test.mocks.dart';
+
+@GenerateMocks([Dio])
+void main() {
+ late MockDio mockDio;
+ late AuthRemoteRepository authRepository;
+ setUp(() {
+ mockDio = MockDio();
+ authRepository = AuthRemoteRepository(dio: mockDio);
+ });
+ group('create (signup)', () {
+ const testName = 'AserMohamed';
+ const testEmail = 'asermohamed@gmail.com';
+ const testDateOfBirth = '2004-11-11';
+
+ test('should return success message on successful signup', () async {
+ final apiResponse = {'message': 'Verification email sent'};
+
+ when(
+ mockDio.post(
+ 'api/auth/signup',
+ data: {
+ 'name': testName,
+ 'email': testEmail,
+ 'dateOfBirth': testDateOfBirth,
+ },
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/signup'),
+ data: apiResponse,
+ statusCode: 201,
+ ),
+ );
+
+ final result = await authRepository.create(
+ name: testName,
+ email: testEmail,
+ dateOfBirth: testDateOfBirth,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail(
+ 'Test failed: Should have returned Right, but got Left($failure)',
+ ),
+ (message) {
+ expect(message, 'Verification email sent');
+ },
+ );
+ });
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post(
+ 'api/auth/signup',
+ data: {
+ 'name': testName,
+ 'email': testEmail,
+ 'dateOfBirth': testDateOfBirth,
+ },
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/signup'),
+ data: apiResponse,
+ statusCode: 201,
+ ),
+ );
+
+ final result = await authRepository.create(
+ name: testName,
+ email: testEmail,
+ dateOfBirth: testDateOfBirth,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Verification email sent');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/signup',
+ data: {
+ 'name': testName,
+ 'email': testEmail,
+ 'dateOfBirth': testDateOfBirth,
+ },
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/signup'),
+ message: 'Network error',
+ ),
+ );
+
+ final result = await authRepository.create(
+ name: testName,
+ email: testEmail,
+ dateOfBirth: testDateOfBirth,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Signup failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post(
+ 'api/auth/signup',
+ data: {
+ 'name': testName,
+ 'email': testEmail,
+ 'dateOfBirth': testDateOfBirth,
+ },
+ ),
+ ).thenThrow(Exception('Unexpected error'));
+
+ final result = await authRepository.create(
+ name: testName,
+ email: testEmail,
+ dateOfBirth: testDateOfBirth,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('verifySignupEmail', () {
+ const testEmail = 'test@example.com';
+ const testCode = '123456';
+
+ test('should return success message on successful verification', () async {
+ final apiResponse = {'message': 'Verified successfully'};
+
+ when(
+ mockDio.post(
+ 'api/auth/verify-signup',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/verify-signup'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.verifySignupEmail(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Verified successfully');
+ },
+ );
+ });
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post(
+ 'api/auth/verify-signup',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/verify-signup'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.verifySignupEmail(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Verified successfully');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/verify-signup',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/verify-signup'),
+ message: 'Invalid code',
+ ),
+ );
+
+ final result = await authRepository.verifySignupEmail(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Email verification failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post(
+ 'api/auth/verify-signup',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenThrow(Exception('Network timeout'));
+
+ final result = await authRepository.verifySignupEmail(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('signup (finalize)', () {
+ const testEmail = 'asermohamed@gmail.com';
+ const testPassword = 'ASERMOHAMED123***aaa';
+
+ final userMap = {
+ 'id': '1',
+ 'name': 'Test User',
+ 'email': testEmail,
+ 'username': 'testuser',
+ 'dateOfBirth': '1990-01-01',
+ 'isEmailVerified': true,
+ 'isVerified': false,
+ };
+
+ final tokensMap = {
+ 'accessToken': 'fake_access_token_123456789',
+ 'refreshToken': 'fake_refresh_token_123456789',
+ };
+
+ final apiResponse = {'user': userMap, 'tokens': tokensMap};
+
+ test(
+ 'should return (UserModel, TokensModel) on successful finalization',
+ () async {
+ when(
+ mockDio.post(
+ 'api/auth/finalize_signup',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/finalize_signup'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.signup(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (successData) {
+ final (user, tokens) = successData;
+ expect(user, isA());
+ expect(user.email, testEmail);
+ expect(user.name, 'Test User');
+ expect(tokens, isA());
+
+ expect(tokens.accessToken, 'fake_access_token_123456789');
+ expect(tokens.refreshToken, 'fake_refresh_token_123456789');
+ },
+ );
+ },
+ );
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/finalize_signup',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/finalize_signup'),
+ message: 'Server error',
+ ),
+ );
+
+ final result = await authRepository.signup(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Signup failed');
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post(
+ 'api/auth/finalize_signup',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenThrow(Exception('Parse error'));
+
+ final result = await authRepository.signup(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should handle malformed response data', () async {
+ final malformedResponse = {
+ 'user': {'id': '1'},
+ 'tokens': {},
+ };
+
+ when(
+ mockDio.post(
+ 'api/auth/finalize_signup',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/finalize_signup'),
+ data: malformedResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.signup(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ });
+ });
+ group('login', () {
+ const testEmail = 'asermohamed@gmail.com';
+ const testPassword = 'ASERMOHAMED123***aaa';
+ final userMap = {
+ 'id': '1',
+ 'name': 'Test User',
+ 'email': testEmail,
+ 'username': 'testuser',
+ };
+ final tokensMap = {
+ 'Token': 'fake_access_token_123456789',
+ 'Refresh_token': 'fake_refresh_token_123456789',
+ };
+
+ final apiResponse = {'user': userMap, ...tokensMap};
+
+ final userModel = UserModel.fromMap(userMap);
+ final tokensModel = TokensModel.fromMap_login(apiResponse);
+
+ test(
+ 'should return (UserModel, TokensModel) on successful login',
+ () async {
+ when(
+ mockDio.post(
+ 'api/auth/login',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/login'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.login(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail(
+ 'Test failed: Should have returned Right, but got Left($failure)',
+ ),
+ (successData) {
+ final (user, tokens) = successData;
+ expect(user, userModel);
+ expect(tokens.accessToken, tokensModel.accessToken);
+ expect(tokens.refreshToken, tokensModel.refreshToken);
+ },
+ );
+ },
+ );
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/login',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/login'),
+ message: 'Connection error',
+ ),
+ );
+
+ final result = await authRepository.login(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isLeft(), true);
+
+ result.fold(
+ (failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Login failed');
+ },
+ (successData) =>
+ fail('Test failed: Should have returned Left, but got Right'),
+ );
+ });
+
+ test(
+ 'should return AppFailure with "Wrong Password" on generic exception',
+ () async {
+ when(
+ mockDio.post(
+ 'api/auth/login',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenThrow(Exception(' error'));
+
+ final result = await authRepository.login(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ result.fold((failure) {
+ expect(failure.message, 'Wrong Password');
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ },
+ );
+ });
+
+ group('updateUsername', () {
+ const testUsername = 'Aser_Mohamed_2025';
+ final currentUser = UserModel(
+ id: '1',
+ name: 'Test User',
+ email: 'test@example.com',
+ username: 'oldusername',
+ dob: '1990-01-01',
+ isEmailVerified: false,
+ isVerified: false,
+ );
+
+ final apiResponse = {
+ 'user': {'username': testUsername},
+ 'tokens': {'access': 'new_access_token', 'refresh': 'new_refresh_token'},
+ };
+
+ test('should return updated UserModel and new tokens', () async {
+ when(
+ mockDio.put(
+ 'api/auth/update_username',
+ data: {'username': testUsername},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/update_username'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.updateUsername(
+ currentUser: currentUser,
+ Username: testUsername,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (successData) {
+ final (user, tokens) = successData;
+ expect(user.username, testUsername);
+ expect(user.id, currentUser.id);
+ expect(user.email, currentUser.email);
+ expect(tokens.accessToken, 'new_access_token');
+ expect(tokens.refreshToken, 'new_refresh_token');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.put(
+ 'api/auth/update_username',
+ data: {'username': testUsername},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/update_username'),
+ message: 'Username already taken',
+ ),
+ );
+
+ final result = await authRepository.updateUsername(
+ currentUser: currentUser,
+ Username: testUsername,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Failed to update username');
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.put(
+ 'api/auth/update_username',
+ data: {'username': testUsername},
+ ),
+ ).thenThrow(Exception('Parse error'));
+
+ final result = await authRepository.updateUsername(
+ currentUser: currentUser,
+ Username: testUsername,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('forget_password', () {
+ const testEmail = 'asermohamed@gmail.com';
+
+ test('should return success message', () async {
+ final apiResponse = {'message': 'Reset code sent'};
+
+ when(
+ mockDio.post('api/auth/forget-password', data: {'email': testEmail}),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/forget-password'),
+ data: apiResponse,
+ ),
+ );
+
+ final result = await authRepository.forget_password(email: testEmail);
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Reset code sent');
+ },
+ );
+ });
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post('api/auth/forget-password', data: {'email': testEmail}),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/forget-password'),
+ data: apiResponse,
+ ),
+ );
+
+ final result = await authRepository.forget_password(email: testEmail);
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Reset code sent');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post('api/auth/forget-password', data: {'email': testEmail}),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/forget-password'),
+ message: 'Email not found',
+ ),
+ );
+
+ final result = await authRepository.forget_password(email: testEmail);
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Forget password failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post('api/auth/forget-password', data: {'email': testEmail}),
+ ).thenThrow(Exception('Network error'));
+
+ final result = await authRepository.forget_password(email: testEmail);
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('verify_reset_code', () {
+ const testEmail = 'asermohamed@gmail.com';
+ const testCode = '123456';
+
+ test('should return success message', () async {
+ final apiResponse = {'message': 'Reset code verified'};
+
+ when(
+ mockDio.post(
+ 'api/auth/verify-reset-code',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/verify-reset-code'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.verify_reset_code(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Reset code verified');
+ },
+ );
+ });
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post(
+ 'api/auth/verify-reset-code',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/verify-reset-code'),
+ data: apiResponse,
+ ),
+ );
+
+ final result = await authRepository.verify_reset_code(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Reset code verified');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/verify-reset-code',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/verify-reset-code'),
+ message: 'Invalid code',
+ ),
+ );
+
+ final result = await authRepository.verify_reset_code(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Verify reset code failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post(
+ 'api/auth/verify-reset-code',
+ data: {'email': testEmail, 'code': testCode},
+ ),
+ ).thenThrow(Exception('Timeout'));
+
+ final result = await authRepository.verify_reset_code(
+ email: testEmail,
+ code: testCode,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('reset_password', () {
+ const testEmail = 'asermohamed@gmail.com';
+ const testPassword = 'ASERMOHAMED123***aaa';
+
+ final userMap = {
+ 'id': '1',
+ 'name': 'Test User',
+ 'email': testEmail,
+ 'username': 'testuser',
+ 'dateOfBirth': '2004-11-11',
+ 'isEmailVerified': false,
+ 'isVerified': false,
+ };
+
+ final apiResponse = {
+ 'user': userMap,
+ 'accesstoken': 'reset_access_token',
+ 'refresh_token': 'reset_refresh_token',
+ };
+
+ test('should return UserModel and TokensModel on success', () async {
+ when(
+ mockDio.post(
+ 'api/auth/reset-password',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/reset-password'),
+ data: apiResponse,
+ ),
+ );
+
+ final result = await authRepository.reset_password(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (successData) {
+ final (user, tokens) = successData;
+ expect(user, isA());
+ expect(user.email, testEmail);
+ expect(tokens, isA());
+ expect(tokens.accessToken, 'reset_access_token');
+ expect(tokens.refreshToken, 'reset_refresh_token');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/reset-password',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/reset-password'),
+ message: 'Server error',
+ ),
+ );
+
+ final result = await authRepository.reset_password(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Reset password failed');
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post(
+ 'api/auth/reset-password',
+ data: {'email': testEmail, 'password': testPassword},
+ ),
+ ).thenThrow(Exception('Parse error'));
+
+ final result = await authRepository.reset_password(
+ email: testEmail,
+ password: testPassword,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (successData) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('uploadProfilePhoto', () {
+ test('should return AppFailure when no file is selected', () async {
+ final pickedImage = PickedImage(file: null, name: 'test.jpg');
+
+ final result = await authRepository.uploadProfilePhoto(
+ pickedImage: pickedImage,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'No file selected');
+ }, (data) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return mediaId and keyName on successful upload', () async {
+ final mockFile = File('test.jpg');
+ final pickedImage = PickedImage(file: mockFile, name: 'test.jpg');
+
+ final uploadRequestResponse = {
+ 'url': 'https://presigned-url.com',
+ 'keyName': 'media/test-key-123',
+ };
+
+ final confirmResponse = {
+ 'newMedia': {'id': 'media-id-123', 'keyName': 'media/test-key-123'},
+ };
+
+ when(
+ mockDio.post(
+ 'api/media/upload-request',
+ data: {'fileName': 'test.jpg', 'contentType': 'image/jpeg'},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/media/upload-request'),
+ data: uploadRequestResponse,
+ statusCode: 200,
+ ),
+ );
+
+ when(
+ mockDio.post('api/media/confirm-upload/media/test-key-123'),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(
+ path: 'api/media/confirm-upload/media/test-key-123',
+ ),
+ data: confirmResponse,
+ statusCode: 200,
+ ),
+ );
+ });
+
+ test(
+ 'should return AppFailure on DioException during upload request',
+ () async {
+ final mockFile = File('test.jpg');
+ final pickedImage = PickedImage(file: mockFile, name: 'test.jpg');
+
+ when(
+ mockDio.post(
+ 'api/media/upload-request',
+ data: {'fileName': 'test.jpg', 'contentType': 'image/jpeg'},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/media/upload-request'),
+ message: 'Server error',
+ ),
+ );
+ },
+ );
+ });
+
+ group('downloadMedia', () {
+ const testMediaId = 'media-123';
+
+ test('should return AppFailure on exception', () async {
+ when(mockDio.get('api/media/download-request/$testMediaId')).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(
+ path: 'api/media/download-request/$testMediaId',
+ ),
+ message: 'Not found',
+ ),
+ );
+
+ final result = await authRepository.downloadMedia(mediaId: testMediaId);
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Download failed'));
+ }, (file) => fail('Test failed: Should have returned Left'));
+ });
+ });
+ group('check_email', () {
+ const testEmail = 'asermohamed@gmail.com';
+
+ test('should return true when email exists', () async {
+ final apiResponse = {'exists': true};
+
+ when(
+ mockDio.post('api/auth/getUser', data: {'email': testEmail}),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/getUser'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.check_email(email: testEmail);
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (exists) {
+ expect(exists, true);
+ },
+ );
+ });
+
+ test('should return false when email does not exist', () async {
+ final apiResponse = {'exists': false};
+
+ when(
+ mockDio.post('api/auth/getUser', data: {'email': testEmail}),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/getUser'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.check_email(email: testEmail);
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (exists) {
+ expect(exists, false);
+ },
+ );
+ });
+
+ test('should return false when exists field is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post('api/auth/getUser', data: {'email': testEmail}),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/getUser'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.check_email(email: testEmail);
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (exists) {
+ expect(exists, false);
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post('api/auth/getUser', data: {'email': testEmail}),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/getUser'),
+ message: 'Network error',
+ ),
+ );
+
+ final result = await authRepository.check_email(email: testEmail);
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Email check failed');
+ }, (exists) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post('api/auth/getUser', data: {'email': testEmail}),
+ ).thenThrow(Exception('Unexpected error'));
+
+ final result = await authRepository.check_email(email: testEmail);
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (exists) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('update_password', () {
+ const testPassword = 'OldPassword123***';
+ const testNewPassword = 'NewPassword123***';
+ const testConfirmPassword = 'NewPassword123***';
+
+ test(
+ 'should return success message on successful password update',
+ () async {
+ final apiResponse = {'message': 'Password updated successfully'};
+
+ when(
+ mockDio.post(
+ 'api/auth/change-password',
+ data: {
+ 'password': testPassword,
+ 'newpassword': testNewPassword,
+ 'confirmPassword': testConfirmPassword,
+ },
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/change-password'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.update_password(
+ password: testPassword,
+ newpassword: testNewPassword,
+ confirmPassword: testConfirmPassword,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Password updated successfully');
+ },
+ );
+ },
+ );
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post(
+ 'api/auth/change-password',
+ data: {
+ 'password': testPassword,
+ 'newpassword': testNewPassword,
+ 'confirmPassword': testConfirmPassword,
+ },
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/change-password'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.update_password(
+ password: testPassword,
+ newpassword: testNewPassword,
+ confirmPassword: testConfirmPassword,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Password updated successfully');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/change-password',
+ data: {
+ 'password': testPassword,
+ 'newpassword': testNewPassword,
+ 'confirmPassword': testConfirmPassword,
+ },
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/change-password'),
+ message: 'Wrong password',
+ ),
+ );
+
+ final result = await authRepository.update_password(
+ password: testPassword,
+ newpassword: testNewPassword,
+ confirmPassword: testConfirmPassword,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'update password failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post(
+ 'api/auth/change-password',
+ data: {
+ 'password': testPassword,
+ 'newpassword': testNewPassword,
+ 'confirmPassword': testConfirmPassword,
+ },
+ ),
+ ).thenThrow(Exception('Network timeout'));
+
+ final result = await authRepository.update_password(
+ password: testPassword,
+ newpassword: testNewPassword,
+ confirmPassword: testConfirmPassword,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('update_email', () {
+ const testNewEmail = 'asermohamed123@gmail.com';
+
+ test('should return success message on successful email update', () async {
+ final apiResponse = {'message': 'Email updated successfully'};
+
+ when(
+ mockDio.post('api/auth/change-email', data: {'newemail': testNewEmail}),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/change-email'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.update_email(newemail: testNewEmail);
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Email updated successfully');
+ },
+ );
+ });
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post('api/auth/change-email', data: {'newemail': testNewEmail}),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/change-email'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.update_email(newemail: testNewEmail);
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'Email updated successfully');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post('api/auth/change-email', data: {'newemail': testNewEmail}),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/change-email'),
+ message: 'Email already in use',
+ ),
+ );
+
+ final result = await authRepository.update_email(newemail: testNewEmail);
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'update email failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post('api/auth/change-email', data: {'newemail': testNewEmail}),
+ ).thenThrow(Exception('Server error'));
+
+ final result = await authRepository.update_email(newemail: testNewEmail);
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('verify_new_email', () {
+ const testNewEmail = 'asermohamed123@gmail.com';
+ const testCode = '123456';
+
+ test('should return success message on successful verification', () async {
+ final apiResponse = {'message': 'updated email successfully'};
+
+ when(
+ mockDio.post(
+ 'api/auth/verify-new-email',
+ data: {'email': testNewEmail, 'code': testCode},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/verify-new-email'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.verify_new_email(
+ newemail: testNewEmail,
+ code: testCode,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'updated email successfully');
+ },
+ );
+ });
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post(
+ 'api/auth/verify-new-email',
+ data: {'email': testNewEmail, 'code': testCode},
+ ),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/auth/verify-new-email'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.verify_new_email(
+ newemail: testNewEmail,
+ code: testCode,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'updated email successfully');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException', () async {
+ when(
+ mockDio.post(
+ 'api/auth/verify-new-email',
+ data: {'email': testNewEmail, 'code': testCode},
+ ),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/auth/verify-new-email'),
+ message: 'Invalid verification code',
+ ),
+ );
+
+ final result = await authRepository.verify_new_email(
+ newemail: testNewEmail,
+ code: testCode,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'Email update failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post(
+ 'api/auth/verify-new-email',
+ data: {'email': testNewEmail, 'code': testCode},
+ ),
+ ).thenThrow(Exception('Network error'));
+
+ final result = await authRepository.verify_new_email(
+ newemail: testNewEmail,
+ code: testCode,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+
+ group('registerFcmToken', () {
+ const testFcmToken = 'fcm_token_123456789';
+
+ test('should return success message on successful registration', () async {
+ final apiResponse = {'message': 'FCM registered successfully'};
+
+ when(
+ mockDio.post('api/users/fcm-token', data: anyNamed('data')),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/users/fcm-token'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.registerFcmToken(
+ fcmToken: testFcmToken,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'FCM registered successfully');
+ },
+ );
+ });
+
+ test('should return default message when message is missing', () async {
+ final apiResponse = {};
+
+ when(
+ mockDio.post('api/users/fcm-token', data: anyNamed('data')),
+ ).thenAnswer(
+ (_) async => Response(
+ requestOptions: RequestOptions(path: 'api/users/fcm-token'),
+ data: apiResponse,
+ statusCode: 200,
+ ),
+ );
+
+ final result = await authRepository.registerFcmToken(
+ fcmToken: testFcmToken,
+ );
+
+ expect(result.isRight(), true);
+ result.fold(
+ (failure) => fail('Test failed: Should have returned Right'),
+ (message) {
+ expect(message, 'FCM registered successfully');
+ },
+ );
+ });
+
+ test('should return AppFailure on DioException with response', () async {
+ when(
+ mockDio.post('api/users/fcm-token', data: anyNamed('data')),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/users/fcm-token'),
+ response: Response(
+ requestOptions: RequestOptions(path: 'api/users/fcm-token'),
+ data: {'message': 'Token registration failed'},
+ statusCode: 400,
+ ),
+ message: 'Bad request',
+ ),
+ );
+
+ final result = await authRepository.registerFcmToken(
+ fcmToken: testFcmToken,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('FCM registration failed'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on DioException without response', () async {
+ when(
+ mockDio.post('api/users/fcm-token', data: anyNamed('data')),
+ ).thenThrow(
+ DioException(
+ requestOptions: RequestOptions(path: 'api/users/fcm-token'),
+ message: 'Connection timeout',
+ ),
+ );
+
+ final result = await authRepository.registerFcmToken(
+ fcmToken: testFcmToken,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, 'FCM registration failed');
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+
+ test('should return AppFailure on generic exception', () async {
+ when(
+ mockDio.post('api/users/fcm-token', data: anyNamed('data')),
+ ).thenThrow(Exception('Unexpected error'));
+
+ final result = await authRepository.registerFcmToken(
+ fcmToken: testFcmToken,
+ );
+
+ expect(result.isLeft(), true);
+ result.fold((failure) {
+ expect(failure, isA());
+ expect(failure.message, contains('Exception'));
+ }, (message) => fail('Test failed: Should have returned Left'));
+ });
+ });
+}
diff --git a/test/features/auth/repositories/auth_remote_repository_test.mocks.dart b/test/features/auth/repositories/auth_remote_repository_test.mocks.dart
new file mode 100644
index 0000000..dae153b
--- /dev/null
+++ b/test/features/auth/repositories/auth_remote_repository_test.mocks.dart
@@ -0,0 +1,806 @@
+// Mocks generated by Mockito 5.4.6 from annotations
+// in lite_x/test/features/auth/repositories/auth_remote_repository_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i8;
+
+import 'package:dio/src/adapter.dart' as _i3;
+import 'package:dio/src/cancel_token.dart' as _i9;
+import 'package:dio/src/dio.dart' as _i7;
+import 'package:dio/src/dio_mixin.dart' as _i5;
+import 'package:dio/src/options.dart' as _i2;
+import 'package:dio/src/response.dart' as _i6;
+import 'package:dio/src/transformer.dart' as _i4;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: must_be_immutable
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeBaseOptions_0 extends _i1.SmartFake implements _i2.BaseOptions {
+ _FakeBaseOptions_0(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+class _FakeHttpClientAdapter_1 extends _i1.SmartFake
+ implements _i3.HttpClientAdapter {
+ _FakeHttpClientAdapter_1(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+class _FakeTransformer_2 extends _i1.SmartFake implements _i4.Transformer {
+ _FakeTransformer_2(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+class _FakeInterceptors_3 extends _i1.SmartFake implements _i5.Interceptors {
+ _FakeInterceptors_3(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+class _FakeResponse_4 extends _i1.SmartFake implements _i6.Response {
+ _FakeResponse_4(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+class _FakeDio_5 extends _i1.SmartFake implements _i7.Dio {
+ _FakeDio_5(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+/// A class which mocks [Dio].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockDio extends _i1.Mock implements _i7.Dio {
+ MockDio() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ _i2.BaseOptions get options =>
+ (super.noSuchMethod(
+ Invocation.getter(#options),
+ returnValue: _FakeBaseOptions_0(this, Invocation.getter(#options)),
+ )
+ as _i2.BaseOptions);
+
+ @override
+ _i3.HttpClientAdapter get httpClientAdapter =>
+ (super.noSuchMethod(
+ Invocation.getter(#httpClientAdapter),
+ returnValue: _FakeHttpClientAdapter_1(
+ this,
+ Invocation.getter(#httpClientAdapter),
+ ),
+ )
+ as _i3.HttpClientAdapter);
+
+ @override
+ _i4.Transformer get transformer =>
+ (super.noSuchMethod(
+ Invocation.getter(#transformer),
+ returnValue: _FakeTransformer_2(
+ this,
+ Invocation.getter(#transformer),
+ ),
+ )
+ as _i4.Transformer);
+
+ @override
+ _i5.Interceptors get interceptors =>
+ (super.noSuchMethod(
+ Invocation.getter(#interceptors),
+ returnValue: _FakeInterceptors_3(
+ this,
+ Invocation.getter(#interceptors),
+ ),
+ )
+ as _i5.Interceptors);
+
+ @override
+ set options(_i2.BaseOptions? _options) => super.noSuchMethod(
+ Invocation.setter(#options, _options),
+ returnValueForMissingStub: null,
+ );
+
+ @override
+ set httpClientAdapter(_i3.HttpClientAdapter? _httpClientAdapter) =>
+ super.noSuchMethod(
+ Invocation.setter(#httpClientAdapter, _httpClientAdapter),
+ returnValueForMissingStub: null,
+ );
+
+ @override
+ set transformer(_i4.Transformer? _transformer) => super.noSuchMethod(
+ Invocation.setter(#transformer, _transformer),
+ returnValueForMissingStub: null,
+ );
+
+ @override
+ void close({bool? force = false}) => super.noSuchMethod(
+ Invocation.method(#close, [], {#force: force}),
+ returnValueForMissingStub: null,
+ );
+
+ @override
+ _i8.Future<_i6.Response> head(
+ String? path, {
+ Object? data,
+ Map? queryParameters,
+ _i2.Options? options,
+ _i9.CancelToken? cancelToken,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(
+ #head,
+ [path],
+ {
+ #data: data,
+ #queryParameters: queryParameters,
+ #options: options,
+ #cancelToken: cancelToken,
+ },
+ ),
+ returnValue: _i8.Future<_i6.Response>.value(
+ _FakeResponse_4(
+ this,
+ Invocation.method(
+ #head,
+ [path],
+ {
+ #data: data,
+ #queryParameters: queryParameters,
+ #options: options,
+ #cancelToken: cancelToken,
+ },
+ ),
+ ),
+ ),
+ )
+ as _i8.Future<_i6.Response>);
+
+ @override
+ _i8.Future<_i6.Response> headUri(
+ Uri? uri, {
+ Object? data,
+ _i2.Options? options,
+ _i9.CancelToken? cancelToken,
+ }) =>
+ (super.noSuchMethod(
+ Invocation.method(
+ #headUri,
+ [uri],
+ {#data: data, #options: options, #cancelToken: cancelToken},
+ ),
+ returnValue: _i8.Future<_i6.Response>.value(
+ _FakeResponse_4(
+ this,
+ Invocation.method(
+ #headUri,
+ [uri],
+ {#data: data, #options: options, #cancelToken: cancelToken},
+ ),
+ ),
+ ),
+ )
+ as _i8.Future<_i6.Response>);
+
+ @override
+ _i8.Future<_i6.Response