feat(auth): Add refresh token functionality

This commit is contained in:
BisratHailu 2026-01-21 17:41:35 +03:00
parent 56fc60e2fa
commit befbfb4727
12 changed files with 186 additions and 144 deletions

View File

@ -29,6 +29,7 @@ import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
// @stacked-import
@StackedApp(
@ -55,6 +56,7 @@ import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
MaterialRoute(page: LearnModuleView),
MaterialRoute(page: WelcomeView),
MaterialRoute(page: AssessmentView),
MaterialRoute(page: LearnLessonView),
// @stacked-route
],
dependencies: [

View File

@ -5,10 +5,10 @@
// **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i24;
import 'package:flutter/material.dart' as _i25;
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i25;
import 'package:stacked_services/stacked_services.dart' as _i26;
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i10;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
@ -18,6 +18,8 @@ import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2;
import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19;
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'
as _i24;
import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20;
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
as _i21;
@ -85,6 +87,8 @@ class Routes {
static const assessmentView = '/assessment-view';
static const learnLessonView = '/learn-lesson-view';
static const all = <String>{
homeView,
onboardingView,
@ -108,6 +112,7 @@ class Routes {
learnModuleView,
welcomeView,
assessmentView,
learnLessonView,
};
}
@ -201,17 +206,21 @@ class StackedRouter extends _i1.RouterBase {
Routes.assessmentView,
page: _i23.AssessmentView,
),
_i1.RouteDef(
Routes.learnLessonView,
page: _i24.LearnLessonView,
),
];
final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(),
settings: data,
);
},
_i3.OnboardingView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(),
settings: data,
);
@ -220,127 +229,133 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(),
);
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data,
);
},
_i5.ProfileView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(),
settings: data,
);
},
_i6.ProfileDetailView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(),
settings: data,
);
},
_i7.DownloadsView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(),
settings: data,
);
},
_i8.ProgressView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(),
settings: data,
);
},
_i9.OngoingProgressView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(),
settings: data,
);
},
_i10.AccountPrivacyView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(),
settings: data,
);
},
_i11.SupportView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(),
settings: data,
);
},
_i12.TelegramSupportView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(),
settings: data,
);
},
_i13.CallSupportView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(),
settings: data,
);
},
_i14.LanguageView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(),
settings: data,
);
},
_i15.PrivacyPolicyView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(),
settings: data,
);
},
_i16.TermsAndConditionsView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(),
settings: data,
);
},
_i17.RegisterView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(),
settings: data,
);
},
_i18.LoginView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(),
settings: data,
);
},
_i19.LearnView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(),
settings: data,
);
},
_i20.LearnLevelView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(),
settings: data,
);
},
_i21.LearnModuleView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(),
settings: data,
);
},
_i22.WelcomeView: (data) {
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(),
settings: data,
);
},
_i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i24.MaterialPageRoute<dynamic>(
return _i25.MaterialPageRoute<dynamic>(
builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data),
settings: data,
);
},
_i24.LearnLessonView: (data) {
return _i25.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(),
settings: data,
);
},
};
@override
@ -356,7 +371,7 @@ class StartupViewArguments {
this.label = 'Loading',
});
final _i24.Key? key;
final _i25.Key? key;
final String label;
@ -383,7 +398,7 @@ class AssessmentViewArguments {
required this.data,
});
final _i24.Key? key;
final _i25.Key? key;
final Map<String, dynamic> data;
@ -404,7 +419,7 @@ class AssessmentViewArguments {
}
}
extension NavigatorStateExtension on _i25.NavigationService {
extension NavigatorStateExtension on _i26.NavigationService {
Future<dynamic> navigateToHomeView([
int? routerId,
bool preventDuplicates = true,
@ -434,7 +449,7 @@ extension NavigatorStateExtension on _i25.NavigationService {
}
Future<dynamic> navigateToStartupView({
_i24.Key? key,
_i25.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -703,7 +718,7 @@ extension NavigatorStateExtension on _i25.NavigationService {
}
Future<dynamic> navigateToAssessmentView({
_i24.Key? key,
_i25.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -719,6 +734,20 @@ extension NavigatorStateExtension on _i25.NavigationService {
transition: transition);
}
Future<dynamic> navigateToLearnLessonView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnLessonView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([
int? routerId,
bool preventDuplicates = true,
@ -748,7 +777,7 @@ extension NavigatorStateExtension on _i25.NavigationService {
}
Future<dynamic> replaceWithStartupView({
_i24.Key? key,
_i25.Key? key,
String label = 'Loading',
int? routerId,
bool preventDuplicates = true,
@ -1017,7 +1046,7 @@ extension NavigatorStateExtension on _i25.NavigationService {
}
Future<dynamic> replaceWithAssessmentView({
_i24.Key? key,
_i25.Key? key,
required Map<String, dynamic> data,
int? routerId,
bool preventDuplicates = true,
@ -1032,4 +1061,18 @@ extension NavigatorStateExtension on _i25.NavigationService {
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnLessonView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnLessonView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
}

View File

@ -130,13 +130,13 @@ class DioService {
if (user.refreshToken == null) return false;
try {
Map<String,dynamic> data = {
Map<String, dynamic> data = {
'role': 'STUDENT',
'user_id': user.userId,
'access_token': user.accessToken,
'refresh_token': user.refreshToken
};
print(data);
print(data);
final response = await _refreshDio.post(
'$baseUrl/$kRefreshTokenUrl',
data: data,

View File

@ -19,7 +19,6 @@ class LearnLessonViewModel extends BaseViewModel {
'status': ProgressStatuses.completed,
'thumbnail': 'assets/images/image_1.png',
'title': '1.2 Talking About Your Surroundings',
},
{
'status': ProgressStatuses.pending,
@ -30,7 +29,6 @@ class LearnLessonViewModel extends BaseViewModel {
List<Map<String, dynamic>> get lessons => _lessons;
// Navigation
void pop() => _navigationService.back();
}

View File

@ -1,5 +1,6 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import '../../../app/app.locator.dart';
import '../../common/enmus.dart';
@ -35,4 +36,6 @@ class LearnModuleViewModel extends BaseViewModel {
List<Map<String, dynamic>> get modules => _modules;
void pop() => _navigationService.back();
Future<void> navigateToLearnLesson() async=> await _navigationService.navigateToLearnLessonView();
}

View File

@ -83,8 +83,8 @@ class OnboardingViewModel extends FormViewModel {
String get selectedRegion => _selectedRegion;
Future<List<String>> getRegions(String country) async =>
[ 'Afar',
Future<List<String>> getRegions(String country) async => [
'Afar',
'SNNPR',
'Amhara',
'Harari',
@ -98,7 +98,6 @@ class OnboardingViewModel extends FormViewModel {
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
// Learning goal

View File

@ -3,6 +3,7 @@ import 'package:iconsax/iconsax.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/finish_practice_sheet.dart';
import '../common/app_colors.dart';
import '../common/enmus.dart';
@ -35,27 +36,45 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
}
}
Future<void> _showSheet(
{required BuildContext context,
required LearnModuleViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, LearnModuleViewModel viewModel) =>
_buildExpansionTileCard(viewModel);
_buildExpansionTileCard(context: context, viewModel: viewModel);
Widget _buildExpansionTileCard(LearnModuleViewModel viewModel) => Container(
Widget _buildExpansionTileCard(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kcVeryLightGrey),
),
child: _buildTileStack(viewModel),
child: _buildTileStack(context: context, viewModel: viewModel),
);
Widget _buildTileStack(LearnModuleViewModel viewModel) => Stack(
Widget _buildTileStack(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
Stack(
children: [
_buildExpansionTile(viewModel),
_buildContainerShaderWrapper()
_buildExpansionTile(context: context, viewModel: viewModel),
_buildContainerShaderState()
],
);
Widget _buildExpansionTile(LearnModuleViewModel viewModel) => ExpansionTile(
Widget _buildExpansionTile(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
ExpansionTile(
textColor: kcDarkGrey,
title: _buildTitle(),
subtitle: _buildContent(),
@ -73,7 +92,8 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
childrenPadding: const EdgeInsets.fromLTRB(70, 15, 15, 15),
showTrailingIcon: status != ProgressStatuses.pending ? true : false,
initiallyExpanded: status == ProgressStatuses.started ? true : false,
children: _buildExpansionTileChildren(viewModel),
children:
_buildExpansionTileChildren(context: context, viewModel: viewModel),
);
Widget _buildIconWrapper() => CircleAvatar(
@ -102,21 +122,28 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
),
);
List<Widget> _buildExpansionTileChildren(LearnModuleViewModel viewModel) =>
[_buildExpansionTileItem(viewModel)];
List<Widget> _buildExpansionTileChildren(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
[_buildExpansionTileItem(context: context, viewModel: viewModel)];
Widget _buildExpansionTileItem(LearnModuleViewModel viewModel) => Column(
Widget _buildExpansionTileItem(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildExpansionTileItemChildren(viewModel),
children: _buildExpansionTileItemChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildExpansionTileItemChildren(
LearnModuleViewModel viewModel) =>
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
[
_buildProgressRow(),
verticalSpaceSmall,
_buildActionButtonWrapper(viewModel)
_buildActionButtonWrapper(context: context, viewModel: viewModel)
];
Widget _buildProgressRow() => Row(
@ -141,16 +168,22 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
style: TextStyle(color: kcDarkGrey),
);
Widget _buildActionButtonWrapper(LearnModuleViewModel viewModel) => SizedBox(
Widget _buildActionButtonWrapper(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
SizedBox(
height: 40,
child: _buildActionButtons(viewModel),
child: _buildActionButtons(context: context, viewModel: viewModel),
);
Widget _buildActionButtons(LearnModuleViewModel viewModel) => Row(
Widget _buildActionButtons(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
Row(
children: [
_buildLessonButtonWrapper(viewModel),
horizontalSpaceSmall,
_buildPracticeButtonWrapper(viewModel)
_buildPracticeButtonWrapper(context: context, viewModel: viewModel)
],
);
@ -159,39 +192,52 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
);
Widget _buildLessonButton(LearnModuleViewModel viewModel) =>
const CustomElevatedButton(
CustomElevatedButton(
height: 15,
borderRadius: 12,
text: 'View Lessons',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
//onTap: () async => await viewModel.navigateToLearnModule(),
onTap: () async => await viewModel.navigateToLearnLesson(),
);
Widget _buildPracticeButtonWrapper(LearnModuleViewModel viewModel) =>
Widget _buildPracticeButtonWrapper(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
Expanded(
child: _buildPracticeButton(viewModel),
child: _buildPracticeButton(context: context, viewModel: viewModel),
);
Widget _buildPracticeButton(LearnModuleViewModel viewModel) =>
const CustomElevatedButton(
Widget _buildPracticeButton(
{required BuildContext context,
required LearnModuleViewModel viewModel}) =>
CustomElevatedButton(
height: 15,
borderRadius: 12,
text: 'View Practices',
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
// onTap: () async => await viewModel.navigateToLearnLevel(),
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
);
Widget _buildSheet(LearnModuleViewModel viewModel) => FinishPracticeSheet(
onTap: viewModel.pop,
);
Widget _buildContainerShaderState() => status == ProgressStatuses.pending
? _buildContainerShaderWrapper()
: Container();
Widget _buildContainerShaderWrapper() => Positioned.fill(
child: _buildContainerShader(),
);
Widget _buildContainerShader() => Container(
decoration: BoxDecoration(
color: kcWhite.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kcWhite.withOpacity(0.75)),
),
);
}

View File

@ -4,8 +4,6 @@ import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_linear_progress_indicator.dart';
class ModuleProgress extends StatelessWidget {
const ModuleProgress({super.key});
@ -52,6 +50,4 @@ class ModuleProgress extends StatelessWidget {
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
);
}

View File

@ -29,7 +29,6 @@ class MotivationCard extends StatelessWidget {
Widget _buildIcon() => Image.asset('assets/images/deer.png');
Widget _buildText() => Expanded(
child: Text(
'Lets keep going — youre more than halfway there!',

View File

@ -1,44 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:yimaru_app/app/app.bottomsheets.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:yimaru_app/ui/views/home/home_viewmodel.dart';
import '../helpers/test_helpers.dart';
void main() {
HomeViewModel getModel() => HomeViewModel();
group('HomeViewmodelTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
group('incrementCounter -', () {
test('When called once should return Counter is: 1', () {
final model = getModel();
model.incrementCounter();
expect(model.counterLabel, 'Counter is: 1');
});
});
group('showBottomSheet -', () {
test(
'When called, should show custom bottom sheet using notice variant',
() {
final bottomSheetService = getAndRegisterBottomSheetService();
final model = getModel();
model.showBottomSheet();
verify(
bottomSheetService.showCustomSheet(
variant: BottomSheetType.notice,
title: ksHomeBottomSheetTitle,
description: ksHomeBottomSheetDescription,
),
);
},
);
});
});
}