Merge branch 'release/0.1.24'

-fix: Apply fix for ArifPay
This commit is contained in:
BisratHailu 2026-05-27 16:06:40 +03:00
commit 2024dd3b6d
35 changed files with 722 additions and 301 deletions

View File

@ -21,7 +21,7 @@
"create_password": "Create password",
"confirm_password": "Confirm password",
"eight_character_minimum": "8 characters minimum",
"password_math": "password match",
"password_match": "password match",
"sign_up_agreement": "By clicking Sign Up, you agree to our Terms of Service and Privacy Policy",
"terms_of_services": "Terms of Service",
"and": "and",

View File

@ -1,7 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
part 'access.g.dart';
@JsonSerializable()
class Access {
final String? reason;
@ -21,15 +20,43 @@ class Access {
@JsonKey(name: 'progress_percent')
final int? progressPercent;
const Access(
{this.reason,
@JsonKey(name: 'progress_percent_precise')
final int? progressPercentPrecise;
const Access({
this.reason,
this.totalCount,
this.isCompleted,
this.isAccessible,
this.completedCount,
this.progressPercent});
this.progressPercent,
this.progressPercentPrecise,
});
factory Access.fromJson(Map<String, dynamic> json) => _$AccessFromJson(json);
Access copyWith({
String? reason,
int? totalCount,
bool? isCompleted,
bool? isAccessible,
int? completedCount,
int? progressPercent,
int? progressPercentPrecise,
}) {
return Access(
reason: reason ?? this.reason,
totalCount: totalCount ?? this.totalCount,
isCompleted: isCompleted ?? this.isCompleted,
isAccessible: isAccessible ?? this.isAccessible,
progressPercentPrecise:
progressPercentPrecise ?? this.progressPercentPrecise,
completedCount: completedCount ?? this.completedCount,
progressPercent: progressPercent ?? this.progressPercent,
);
}
factory Access.fromJson(Map<String, dynamic> json) =>
_$AccessFromJson(json);
Map<String, dynamic> toJson() => _$AccessToJson(this);
}

View File

@ -13,6 +13,8 @@ Access _$AccessFromJson(Map<String, dynamic> json) => Access(
isAccessible: json['is_accessible'] as bool?,
completedCount: (json['completed_count'] as num?)?.toInt(),
progressPercent: (json['progress_percent'] as num?)?.toInt(),
progressPercentPrecise:
(json['progress_percent_precise'] as num?)?.toInt(),
);
Map<String, dynamic> _$AccessToJson(Access instance) => <String, dynamic>{
@ -22,4 +24,5 @@ Map<String, dynamic> _$AccessToJson(Access instance) => <String, dynamic>{
'is_accessible': instance.isAccessible,
'completed_count': instance.completedCount,
'progress_percent': instance.progressPercent,
'progress_percent_precise': instance.progressPercentPrecise,
};

View File

@ -1,42 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/module_progress.dart';
import 'access.dart';
part 'course_progress.g.dart';
@JsonSerializable()
class CourseProgress {
final String? level;
final int? id;
final String? title;
final String? name;
final String? description;
final Access? access;
@JsonKey(name: 'is_locked')
final bool? isLocked;
final List<ModuleProgress>? modules;
@JsonKey(name: 'sub_course_id')
final int? courseId;
@JsonKey(name: 'display_order')
final int? displayOrder;
@JsonKey(name: 'program_id')
final int? programId;
@JsonKey(name: 'progress_status')
final String? progressStatus;
@JsonKey(name: 'progress_percentage')
final double? progressPercentage;
const CourseProgress(
{this.level,
this.title,
this.isLocked,
this.courseId,
this.description,
this.displayOrder,
this.progressStatus,
this.progressPercentage});
{this.id,this.name,this.access,this.modules,this.programId});
factory CourseProgress.fromJson(Map<String, dynamic> json) =>
_$CourseProgressFromJson(json);
factory CourseProgress.fromJson(Map<String, dynamic> json) => _$CourseProgressFromJson(json);
Map<String, dynamic> toJson() => _$CourseProgressToJson(this);
}

View File

@ -8,24 +8,22 @@ part of 'course_progress.dart';
CourseProgress _$CourseProgressFromJson(Map<String, dynamic> json) =>
CourseProgress(
level: json['level'] as String?,
title: json['title'] as String?,
isLocked: json['is_locked'] as bool?,
courseId: (json['sub_course_id'] as num?)?.toInt(),
description: json['description'] as String?,
displayOrder: (json['display_order'] as num?)?.toInt(),
progressStatus: json['progress_status'] as String?,
progressPercentage: (json['progress_percentage'] as num?)?.toDouble(),
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
access: json['access'] == null
? null
: Access.fromJson(json['access'] as Map<String, dynamic>),
modules: (json['modules'] as List<dynamic>?)
?.map((e) => ModuleProgress.fromJson(e as Map<String, dynamic>))
.toList(),
programId: (json['program_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$CourseProgressToJson(CourseProgress instance) =>
<String, dynamic>{
'level': instance.level,
'title': instance.title,
'description': instance.description,
'is_locked': instance.isLocked,
'sub_course_id': instance.courseId,
'display_order': instance.displayOrder,
'progress_status': instance.progressStatus,
'progress_percentage': instance.progressPercentage,
'id': instance.id,
'name': instance.name,
'access': instance.access,
'modules': instance.modules,
'program_id': instance.programId,
};

View File

@ -19,13 +19,33 @@ class LearnCourse {
@JsonKey(name: 'program_id')
final int? programId;
const LearnCourse(
{this.id,
const LearnCourse({
this.id,
this.name,
this.access,
this.programId,
this.sortOrder,
this.description});
this.description,
});
LearnCourse copyWith({
int? id,
String? name,
Access? access,
int? sortOrder,
int? programId,
String? description,
}) {
return LearnCourse(
id: id ?? this.id,
name: name ?? this.name,
access: access ?? this.access,
sortOrder: sortOrder ?? this.sortOrder,
programId: programId ?? this.programId,
description: description ?? this.description,
);
}
factory LearnCourse.fromJson(Map<String, dynamic> json) =>
_$LearnCourseFromJson(json);

View File

@ -3,7 +3,6 @@ import 'package:json_annotation/json_annotation.dart';
import 'access.dart';
part 'learn_lesson.g.dart';
@JsonSerializable()
class LearnLesson {
final int? id;
@ -36,6 +35,29 @@ class LearnLesson {
this.description,
});
LearnLesson copyWith({
int? id,
String? title,
int? moduleId,
int? sortOrder,
Access? access,
String? videoUrl,
String? thumbnail,
String? description,
}) {
return LearnLesson(
id: id ?? this.id,
title: title ?? this.title,
access: access ?? this.access,
videoUrl: videoUrl ?? this.videoUrl,
moduleId: moduleId ?? this.moduleId,
sortOrder: sortOrder ?? this.sortOrder,
thumbnail: thumbnail ?? this.thumbnail,
description: description ?? this.description,
);
}
factory LearnLesson.fromJson(Map<String, dynamic> json) =>
_$LearnLessonFromJson(json);

View File

@ -2,7 +2,6 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/access.dart';
part 'learn_module.g.dart';
@JsonSerializable()
class LearnModule {
final int? id;
@ -24,15 +23,40 @@ class LearnModule {
@JsonKey(name: 'sort_order')
final int? sortOrder;
const LearnModule(
{this.id,
const LearnModule({
this.id,
this.icon,
this.name,
this.access,
this.courseId,
this.sortOrder,
this.programId,
this.description});
this.description,
});
LearnModule copyWith({
int? id,
String? icon,
String? name,
int? courseId,
Access? access,
int? programId,
int? sortOrder,
String? description,
}) {
return LearnModule(
id: id ?? this.id,
icon: icon ?? this.icon,
name: name ?? this.name,
access: access ?? this.access,
courseId: courseId ?? this.courseId,
sortOrder: sortOrder ?? this.sortOrder,
programId: programId ?? this.programId,
description: description ?? this.description,
);
}
factory LearnModule.fromJson(Map<String, dynamic> json) =>
_$LearnModuleFromJson(json);

View File

@ -2,7 +2,6 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/access.dart';
part 'learn_program.g.dart';
@JsonSerializable()
class LearnProgram {
final int? id;
@ -16,8 +15,29 @@ class LearnProgram {
@JsonKey(name: 'sort_order')
final int? sortOrder;
const LearnProgram(
{this.id, this.name, this.access, this.sortOrder, this.description});
const LearnProgram({
this.id,
this.name,
this.access,
this.sortOrder,
this.description,
});
LearnProgram copyWith({
int? id,
String? name,
Access? access,
int? sortOrder,
String? description,
}) {
return LearnProgram(
id: id ?? this.id,
name: name ?? this.name,
access: access ?? this.access,
sortOrder: sortOrder ?? this.sortOrder,
description: description ?? this.description,
);
}
factory LearnProgram.fromJson(Map<String, dynamic> json) =>
_$LearnProgramFromJson(json);

View File

@ -0,0 +1,25 @@
import 'package:json_annotation/json_annotation.dart';
import 'access.dart';
part 'lesson_progress.g.dart';
@JsonSerializable()
class LessonProgress {
final int? id;
final String? title;
final Access? access;
@JsonKey(name: 'module_id')
final int? moduleId;
const LessonProgress(
{this.id,this.title,this.access,this.moduleId});
factory LessonProgress.fromJson(Map<String, dynamic> json) => _$LessonProgressFromJson(json);
Map<String, dynamic> toJson() => _$LessonProgressToJson(this);
}

View File

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'lesson_progress.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LessonProgress _$LessonProgressFromJson(Map<String, dynamic> json) =>
LessonProgress(
id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?,
access: json['access'] == null
? null
: Access.fromJson(json['access'] as Map<String, dynamic>),
moduleId: (json['module_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$LessonProgressToJson(LessonProgress instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'access': instance.access,
'module_id': instance.moduleId,
};

View File

@ -0,0 +1,36 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/lesson_progress.dart';
import 'access.dart';
part 'module_progress.g.dart';
@JsonSerializable()
class ModuleProgress {
final int? id;
final String? name;
final Access? access;
final List<LessonProgress>? lessons;
@JsonKey(name: 'course_id')
final int? courseId;
@JsonKey(name: 'program_id')
final int? programId;
const ModuleProgress(
{this.id,
this.name,
this.access,
this.lessons,
this.courseId,
this.programId});
factory ModuleProgress.fromJson(Map<String, dynamic> json) =>
_$ModuleProgressFromJson(json);
Map<String, dynamic> toJson() => _$ModuleProgressToJson(this);
}

View File

@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'module_progress.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ModuleProgress _$ModuleProgressFromJson(Map<String, dynamic> json) =>
ModuleProgress(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
access: json['access'] == null
? null
: Access.fromJson(json['access'] as Map<String, dynamic>),
lessons: (json['lessons'] as List<dynamic>?)
?.map((e) => LessonProgress.fromJson(e as Map<String, dynamic>))
.toList(),
courseId: (json['course_id'] as num?)?.toInt(),
programId: (json['program_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$ModuleProgressToJson(ModuleProgress instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'access': instance.access,
'lessons': instance.lessons,
'course_id': instance.courseId,
'program_id': instance.programId,
};

View File

@ -0,0 +1,24 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/course_progress.dart';
import 'access.dart';
part 'progress_summary.g.dart';
@JsonSerializable()
class ProgressSummary {
final int? id;
final String? name;
final Access? access;
final List<CourseProgress>? courses;
const ProgressSummary(
{this.id,this.name,this.access,this.courses});
factory ProgressSummary.fromJson(Map<String, dynamic> json) => _$ProgressSummaryFromJson(json);
Map<String, dynamic> toJson() => _$ProgressSummaryToJson(this);
}

View File

@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'progress_summary.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProgressSummary _$ProgressSummaryFromJson(Map<String, dynamic> json) =>
ProgressSummary(
id: (json['id'] as num?)?.toInt(),
name: json['name'] as String?,
access: json['access'] == null
? null
: Access.fromJson(json['access'] as Map<String, dynamic>),
courses: (json['courses'] as List<dynamic>?)
?.map((e) => CourseProgress.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$ProgressSummaryToJson(ProgressSummary instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'access': instance.access,
'courses': instance.courses,
};

View File

@ -0,0 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
part 'refresh_object.g.dart';
@JsonSerializable()
class RefreshObject {
final String? url;
@JsonKey(name: 'object_key')
final String? objectKey;
const RefreshObject({this.url, this.objectKey});
factory RefreshObject.fromJson(Map<String, dynamic> json) =>
_$RefreshObjectFromJson(json);
Map<String, dynamic> toJson() => _$RefreshObjectToJson(this);
}

View File

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'refresh_object.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
RefreshObject _$RefreshObjectFromJson(Map<String, dynamic> json) =>
RefreshObject(
url: json['url'] as String?,
objectKey: json['object_key'] as String?,
);
Map<String, dynamic> _$RefreshObjectToJson(RefreshObject instance) =>
<String, dynamic>{
'url': instance.url,
'object_key': instance.objectKey,
};

View File

@ -6,6 +6,7 @@ import 'package:yimaru_app/models/learn_program.dart';
import 'package:yimaru_app/models/assessment_question.dart';
import 'package:yimaru_app/models/course_catalog.dart';
import 'package:yimaru_app/models/course_lesson.dart';
import 'package:yimaru_app/models/refresh_object.dart';
import 'package:yimaru_app/models/user.dart';
import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
@ -20,6 +21,7 @@ import '../models/learn_question.dart';
import '../models/learn_subscription.dart';
import '../models/assessment.dart';
import '../models/learn_subscription_request.dart';
import '../models/progress_summary.dart';
import '../ui/common/enmus.dart';
class ApiService {
@ -621,6 +623,34 @@ class ApiService {
}
}
// Complete profile
Future<Map<String, dynamic>> refreshObject(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kFilesUrl/$kRefreshUrl',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Operation successful',
'data': RefreshObject.fromJson(response.data['data'])
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Learn learn programs
Future<List<LearnProgram>> getLearnPrograms() async {
try {
@ -841,6 +871,30 @@ class ApiService {
}
}
// Get progress summary
Future<List<ProgressSummary>> getProgressSummary() async {
try {
List<ProgressSummary> summaries = [];
final Response response = await _service.dio
.get('$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kLmsUrl/$kProgressSummary');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['data']['programs'] as List;
summaries = decodedData.map(
(e) {
return ProgressSummary.fromJson(e);
},
).toList();
return summaries;
}
return [];
} catch (e) {
return [];
}
}
// Complete lesson
Future<Map<String, dynamic>> completeLearnPractice(int id) async {
try {
@ -898,6 +952,32 @@ class ApiService {
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kPaymentsUrl/$kSubscribeUrl',
data: data);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Subscription successful!',
'data': LearnSubscriptionRequest.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Verify subscription
Future<Map<String, dynamic>> verifySubscription(int id) async {
try {
Response response = await _service.dio.get(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kPaymentsUrl/$kVerifySubscriptionUrl/$id');
if (response.statusCode == 200) {
return {
'message': 'Lesson completed',

View File

@ -1,10 +1,15 @@
import 'package:http/http.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/refresh_object.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../app/app.locator.dart';
import '../models/access.dart';
import '../models/learn_course.dart';
import '../models/learn_lesson.dart';
import '../models/learn_module.dart';
import '../models/learn_program.dart';
import '../models/progress_summary.dart';
import 'api_service.dart';
class LearnService with ListenableServiceMixin {
@ -36,6 +41,24 @@ class LearnService with ListenableServiceMixin {
List<LearnLesson> get lessons => _lessons;
// Learn progress
List<ProgressSummary> _summaries = [];
List<ProgressSummary> get summaries => _summaries;
// Learn programs
Future<String?> refreshObject(String url) async {
Map<String, dynamic> data = {'reference': url};
Map<String, dynamic> response = await _apiService.refreshObject(data);
if (response['status'] == ResponseStatus.success) {
RefreshObject object = response['data'] as RefreshObject;
return object.url ?? '';
}
return null;
}
// Learn programs
Future<void> getLearnPrograms() async {
_programs = await _apiService.getLearnPrograms();
@ -63,4 +86,77 @@ class LearnService with ListenableServiceMixin {
_lessons.sort((a, b) => (a.sortOrder ?? 0).compareTo(b.sortOrder ?? 0));
notifyListeners();
}
// Learn progress
Future<void> getLearnProgressSummary() async {
final summaries = await _apiService.getProgressSummary();
print('MY SUMMARIES: ${summaries.length}');
/// PROGRAM ACCESS MAP
final Map<int, Access?> programAccessMap = {};
/// COURSE ACCESS MAP
final Map<int, Access?> courseAccessMap = {};
/// MODULE ACCESS MAP
final Map<int, Access?> moduleAccessMap = {};
/// LESSON ACCESS MAP
final Map<int, Access?> lessonAccessMap = {};
// Build maps
for (final summary in summaries) {
if (summary.id != null) {
programAccessMap[summary.id!] = summary.access;
}
for (final course in summary.courses ?? []) {
if (course.id != null) {
courseAccessMap[course.id!] = course.access;
}
for (final module in course.modules ?? []) {
if (module.id != null) {
moduleAccessMap[module.id!] = module.access;
}
for (final lesson in module.lessons ?? []) {
if (lesson.id != null) {
lessonAccessMap[lesson.id!] = lesson.access;
}
}
}
}
}
/// UPDATE PROGRAMS
_programs = _programs.map((program) {
return program.copyWith(
access: programAccessMap[program.id] ?? program.access,
);
}).toList();
/// UPDATE COURSES
_courses = _courses.map((course) {
return course.copyWith(
access: courseAccessMap[course.id] ?? course.access,
);
}).toList();
/// UPDATE MODULES
_modules = _modules.map((module) {
return module.copyWith(
access: moduleAccessMap[module.id] ?? module.access,
);
}).toList();
/// UPDATE LESSONS
_lessons = _lessons.map((lesson) {
return lesson.copyWith(
access: lessonAccessMap[lesson.id] ?? lesson.access,
);
}).toList();
notifyListeners();
}
}

View File

@ -130,7 +130,7 @@ class NotificationService {
}
Future<void> updateFCMToken() async {
print('DEVICE TOKEN: ${await _messaging.getToken()}');
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
_messaging.onTokenRefresh.listen((newToken) {
// updateTokenOnServer(newToken);
});

View File

@ -1,6 +1,8 @@
// Endpoints
String kBaseUrl = 'https://api.yimaruacademy.com';
String kLmsUrl = 'lms';
String kAppUrl = 'app';
String kApiUrl = 'api';
@ -9,6 +11,8 @@ String kUnitsUrl = 'units';
String kCheckUrl = 'check';
String kFilesUrl = 'files';
String kApiVersionUrl = 'v1';
String kLevelsUrl = 'levels';
@ -31,13 +35,15 @@ String kCompleteUrl = 'complete';
String kPaymentsUrl = 'payments';
String kExamPrepUrl = 'exam-prep';
String kSubscribeUrl = 'subscribe';
String kPracticesUrl = 'practices';
String kQuestionsUrl = 'questions';
String kExamPrepUrl = 'exam-prep';
String kRefreshUrl = 'refresh-url';
String kCoursePractice = 'by-owner';
@ -57,12 +63,16 @@ String kFieldOptions = 'field-options';
String kResetPassword = 'resetPassword';
String kVerifySubscriptionUrl = 'verify';
String kQuestionSetsUrl = 'question-sets';
String kRequestResetCode = 'sendResetCode';
String kSubcategoriesUrl = 'sub-categories';
String kProgressSummary = 'progress-summary';
String kPublishedVideos = 'videos/published';
String kCoursePracticeQuestions = 'questions';

View File

@ -223,7 +223,7 @@ static const Map<String,dynamic> _en = {
"create_password": "Create password",
"confirm_password": "Confirm password",
"eight_character_minimum": "8 characters minimum",
"password_math": "password match",
"password_match": "password match",
"sign_up_agreement": "By clicking Sign Up, you agree to our Terms of Service and Privacy Policy",
"terms_of_services": "Terms of Service",
"and": "and",

View File

@ -15,17 +15,10 @@ class ArifPayView extends StackedView<ArifPayViewModel> {
void _pop(ArifPayViewModel viewModel) => viewModel.pop;
Future<void> _error() async {
// await Navigator.pushNamed(context, AppRoutes.subscriptionErrorPage);
// Navigation.pop();
}
void _error(ArifPayViewModel viewModel) => viewModel.pop();
void _success() {
// Navigation.navigateTo(
// AppRoutes.subscriptionSuccessPage,
// arguments: widget.body,
// );
}
Future<void> _success(ArifPayViewModel viewModel) async =>
await viewModel.replaceWithHome();
@override
void onViewModelReady(ArifPayViewModel viewModel) async {
@ -58,21 +51,12 @@ class ArifPayView extends StackedView<ArifPayViewModel> {
Widget _buildBody(ArifPayViewModel viewModel) => InAppWebView(
initialUrlRequest:
URLRequest(url: WebUri(viewModel.request?.paymentUrl ?? '')),
onUpdateVisitedHistory: (controller, url, androidIsReload) {
if (url
.toString()
.contains("https://checkout.arifpay.net/canceled")) {
showErrorToast('Operation was cancelled');
// _pop();
} else if (url.toString().contains(kSuccessUrl)) {
_success();
onUpdateVisitedHistory: (controller, url, androidIsReload) async {
if (url.toString().contains(kSuccessUrl)) {
showSuccessToast('Subscription successful, activation in progress!');
_success(viewModel);
} else if (url.toString().contains(kErrorUrl)) {
showErrorToast('Operation was cancelled');
// _pop();
} else if (url.toString().contains("http://x.com/elonmusk/status/")) {
_error();
} else if (url.toString().contains(kErrorUrl)) {
_error();
_error(viewModel);
}
},
);

View File

@ -3,6 +3,7 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../app/app.router.dart';
import '../../../models/learn_subscription_request.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
@ -24,6 +25,9 @@ class ArifPayViewModel extends BaseViewModel {
// Navigation
void pop() => _navigationService.back();
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShow(Routes.homeView);
// Remote api call
// Learn subscription
@ -36,7 +40,8 @@ class ArifPayViewModel extends BaseViewModel {
Map<String, dynamic> data = {
'plan_id': 1,
'phone': '251$phone',
'email': 'test@gmail.com'
'provider': 'ARIFPAY',
'email': 'test@gmail.com',
};
Map<String, dynamic> response =
@ -48,24 +53,4 @@ class ArifPayViewModel extends BaseViewModel {
}
}
//
// Future<void> verifyLearnSubscription(String id) async => await runBusyFuture(_verifyLearnSubscription(phone),
// busyObject: StateObjects.learnSubscription);
//
// Future<void> _verifyLearnSubscription(String id) async {
// if (await _statusChecker.checkConnection()) {
// Map<String,dynamic> data = {
// 'plan_id':1,
// 'phone': '251$phone',
// 'email':'test@gmail.com'
// };
//
// Map<String, dynamic> response =
// await _apiService.createSubscriptionRequest(data);
//
// if (response['status'] == ResponseStatus.success) {
// _request = response['data'];
// }
// }
// }
}

View File

@ -10,6 +10,7 @@ import '../../../app/app.locator.dart';
import '../../../models/learn_module.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/helper_functions.dart';
class LearnLessonViewModel extends ReactiveViewModel {
// Dependency injection
@ -23,6 +24,11 @@ class LearnLessonViewModel extends ReactiveViewModel {
List<ListenableServiceMixin> get listenableServices => [_learnService];
// Learn lessons
final Map<int, String> _refreshedThumbnails = {};
Map<int, String> get refreshedThumbnails => _refreshedThumbnails;
List<LearnLesson> get _lessons => _learnService.lessons;
List<LearnLesson> get lessons => _lessons;
@ -55,6 +61,27 @@ class LearnLessonViewModel extends ReactiveViewModel {
Future<void> _getLessons(int id) async {
if (await _statusChecker.checkConnection()) {
await _learnService.getLearnLessons(id);
// await refreshLessonImages(_lessons);
}
}
//Refresh image
Future<void> refreshLessonImages(List<LearnLesson> lessons) async {
for (final lesson in lessons) {
final thumbnail = lesson.thumbnail;
if (lesson.id == null || thumbnail == null || thumbnail.isEmpty) {
continue;
}
final String? refreshedUrl = await _learnService.refreshObject(thumbnail);
if (refreshedUrl != null) {
_refreshedThumbnails[lesson.id!] = refreshedUrl;
}
}
}
String getLessonImage(LearnLesson lesson) =>
getReadableUrl(_refreshedThumbnails[lesson.id] ?? '') ?? '';
}

View File

@ -3,12 +3,14 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/learn_module.dart';
import 'package:yimaru_app/models/refresh_object.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import '../../../app/app.locator.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/helper_functions.dart';
class LearnModuleViewModel extends ReactiveViewModel {
// Dependency injection
@ -22,6 +24,10 @@ class LearnModuleViewModel extends ReactiveViewModel {
List<ListenableServiceMixin> get listenableServices => [_learnService];
// Learn module
final Map<int, String> _refreshedIcons = {};
Map<int, String> get refreshedIcons => _refreshedIcons;
List<LearnModule> get _modules => _learnService.modules;
List<LearnModule> get modules => _modules;
@ -52,6 +58,28 @@ class LearnModuleViewModel extends ReactiveViewModel {
Future<void> _getLearnModules(int id) async {
if (await _statusChecker.checkConnection()) {
await _learnService.getLearnModules(id);
await refreshModuleImages(_modules);
}
}
//Refresh image
Future<void> refreshModuleImages(List<LearnModule> modules) async {
for (final module in modules) {
final icon = module.icon;
if (module.id == null || icon == null || icon.isEmpty) {
continue;
}
final String? refreshedUrl = await _learnService.refreshObject(icon);
if (refreshedUrl != null) {
_refreshedIcons[module.id!] = refreshedUrl;
}
}
}
String getModuleImage(LearnModule module) =>
getReadableUrl(_refreshedIcons[module.id] ?? '') ?? '';
}

View File

@ -16,6 +16,7 @@ import '../../../services/audio_player_service.dart';
import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/app_colors.dart';
import '../../common/helper_functions.dart';
class LearnPracticeViewModel extends ReactiveViewModel {
// Dependency injection
@ -94,10 +95,16 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Voice? get playing => _playing;
// Learn practices
List<LearnPractice> _practices = [];
List<LearnPractice> get practices => _practices;
final Map<int, String> _refreshedImages= {};
Map<int, String> get refreshedImages => _refreshedImages;
// Practice questions
List<LearnQuestion> _questions = [];
@ -244,6 +251,7 @@ class LearnPracticeViewModel extends ReactiveViewModel {
);
await playVoicePrompt(_questions[index]);
} else {
await completeLearnPractices();
goTo(3);
}
}
@ -260,6 +268,10 @@ class LearnPracticeViewModel extends ReactiveViewModel {
// Remote api call
// Refresh url
Future<String?> refreshUrl(String url) async =>
await _learnService.refreshObject(url);
// Learn practice
Future<void> getLearnPractices(
{required int id, required LearnPractices practice}) async =>
@ -271,14 +283,14 @@ class LearnPracticeViewModel extends ReactiveViewModel {
if (await _statusChecker.checkConnection()) {
if (practice == LearnPractices.course) {
_practices = await _apiService.getLearnCoursePractices(id);
// await refreshPracticeImages(_practices);
await _getLearnPracticeQuestions(_practices.first.questionSetId ?? 0);
} else if (practice == LearnPractices.module) {
_practices = await _apiService.getLearnModulePractices(id);
// await refreshPracticeImages(_practices);
await _getLearnPracticeQuestions(_practices.first.questionSetId ?? 0);
} else {
_practices = await _apiService.getLearnLessonPractices(id);
await _getLearnPracticeQuestions(_practices.first.questionSetId ?? 0);
}
}
@ -296,6 +308,28 @@ class LearnPracticeViewModel extends ReactiveViewModel {
Future<void> _completeLearnPractices() async {
if (await _statusChecker.checkConnection()) {
await _apiService.completeLearnPractice(_practices.first.id ?? 0);
await _learnService.getLearnProgressSummary();
}
}
//Refresh image
Future<void> refreshPracticeImages(List<LearnPractice> practices) async {
for (final practice in practices) {
final image = practice.storyImage;
if (practice.id == null || image == null || image.isEmpty) {
continue;
}
final String? refreshedUrl = await _learnService.refreshObject(image);
if (refreshedUrl != null) {
_refreshedImages[practice.id!] = refreshedUrl;
}
}
}
String getPracticeImage(LearnPractice practice) =>
getReadableUrl(_refreshedImages[practice.id] ?? '') ?? '';
}

View File

@ -15,8 +15,6 @@ class LearnPracticeAppreciationScreen
extends ViewModelWidget<LearnPracticeViewModel> {
const LearnPracticeAppreciationScreen({super.key});
Future<void> _reset(LearnPracticeViewModel viewModel) async =>
await viewModel.reset();
Future<void> _cancel(LearnPracticeViewModel viewModel) async {
await viewModel.stopRecording();

View File

@ -163,10 +163,13 @@ class LearnPracticeDescriptionScreen
Widget _buildImage(LearnPracticeViewModel viewModel) => CachedNetworkImage(
fit: BoxFit.cover,
width: double.maxFinite,
imageUrl:
getReadableUrl(viewModel.practices.first.storyImage ?? '') ?? '',
getReadableUrl( viewModel.practices.first.storyImage ?? '') ?? '',
);
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),

View File

@ -82,7 +82,6 @@ class LearnSubscriptionViewModel extends FormViewModel {
Future<void> _getLearnSubscriptions() async {
if (await _statusChecker.checkConnection()) {
_subscriptions = await _apiService.getLearnSubscriptions();
_subscriptions = _subscriptions + _subscriptions + _subscriptions;
}
}
}

View File

@ -6,6 +6,7 @@ import 'package:yimaru_app/ui/widgets/phone_number_prefix.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/learn_subscription_card.dart';
import '../../../widgets/small_app_bar.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../learn_subscription_view.form.dart';
@ -73,11 +74,15 @@ class LearnSubscriptionFormScreen
);
List<Widget> _buildSheetChildren(LearnSubscriptionViewModel viewModel) => [
verticalSpaceMedium,
verticalSpaceSmall,
_buildTitleWrapper(),
verticalSpaceTiny,
verticalSpaceMedium,
_buildFirstCard(),
verticalSpaceMedium,
_buildSecondCard(),
verticalSpaceLarge,
_buildSubtitle(),
verticalSpaceMassive,
verticalSpaceMedium,
_buildPhoneNumberWrapper(viewModel),
if (viewModel.hasPhoneNumberValidationMessage &&
viewModel.focusPhoneNumber)
@ -87,10 +92,22 @@ class LearnSubscriptionFormScreen
_buildPhoneNumberValidatorWrapper(viewModel),
verticalSpaceLarge,
_buildContinueButton(viewModel),
verticalSpaceMassive,
verticalSpaceMedium,
_buildSecurePaymentWrapper()
];
Widget _buildFirstCard() => const LearnSubscriptionCard(
icon: Icons.school,
title: '180+ New Lessons',
subtitle: 'Access fresh, advanced content',
);
Widget _buildSecondCard() => const LearnSubscriptionCard(
icon: Icons.developer_board,
title: 'Mastery Through Practice',
subtitle: 'Practice All Lessons, Modules & Levels',
);
Widget _buildTitleWrapper() => Align(
alignment: Alignment.center,
child: _buildTitle(),

View File

@ -1,156 +0,0 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/models/course_detail.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../common/app_colors.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
class CourseTile extends StatelessWidget {
final CourseDetail courseDetail;
final GestureTapCallback? onCourseTap;
final GestureTapCallback? onPracticeTap;
const CourseTile({
super.key,
this.onCourseTap,
this.onPracticeTap,
required this.courseDetail,
});
@override
Widget build(BuildContext context) => _buildExpansionTileCard();
Widget _buildExpansionTileCard() => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: kcPrimaryColor.withOpacity(0.2),
),
),
child: _buildTileStack(),
);
Widget _buildTileStack() => Stack(
children: [_buildExpansionTile(), _buildTileShaderState()],
);
Widget _buildExpansionTile() => ExpansionTile(
title: _buildTitle(),
textColor: kcDarkGrey,
showTrailingIcon: false,
initiallyExpanded: false,
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
backgroundColor: kcPrimaryColor.withOpacity(0.1),
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1),
childrenPadding: const EdgeInsets.symmetric(horizontal: 15),
enabled: courseDetail.courseProgress?.progressStatus == 'NOT_STARTED'
? courseDetail.courseProgress?.displayOrder == 1
? true
: false
: true,
children: _buildExpansionTileChildren(),
);
List<Widget> _buildExpansionTileChildren() => [
_buildProgressRow(),
verticalSpaceSmall,
_buildActionButtonWrapper(),
verticalSpaceSmall
];
Widget _buildTitle() => Text(
(courseDetail.course?.title == null ||
(courseDetail.course?.title?.isEmpty ?? false))
? 'Course ${courseDetail.course?.id}'
: courseDetail.course?.title ?? '',
style: style16P600,
);
Widget _buildProgressRow() => Row(
mainAxisSize: MainAxisSize.min,
children: _buildProgressChildren(),
);
List<Widget> _buildProgressChildren() =>
[_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()];
Widget _buildProgressStatusWrapper() => Expanded(
child: _buildProgressStatus(),
);
Widget _buildProgressStatus() => CustomLinearProgressIndicator(
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
progress: courseDetail.courseProgress?.progressPercentage ?? 0 / 100,
);
Widget _buildProgress() => Text(
'${courseDetail.courseProgress?.progressPercentage?.toInt() ?? 0}%',
style: style14DG400,
);
Widget _buildActionButtonWrapper() => SizedBox(
height: 40,
width: 300,
child: _buildActionButtons(),
);
Widget _buildActionButtons() => Row(
children: [
_buildStartButtonWrapper(),
horizontalSpaceSmall,
_buildPracticeButtonWrapper()
],
);
Widget _buildStartButtonWrapper() => Expanded(
child: _buildStartButton(),
);
Widget _buildStartButton() => CustomElevatedButton(
height: 15,
borderRadius: 8,
onTap: onCourseTap,
text: 'Start Course',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
);
Widget _buildPracticeButtonWrapper() => Expanded(
child: _buildPracticeButton(),
);
Widget _buildPracticeButton() => CustomElevatedButton(
height: 15,
borderRadius: 8,
text: 'Practice',
onTap: onPracticeTap,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
);
Widget _buildTileShaderState() =>
courseDetail.courseProgress?.progressStatus == 'NOT_STARTED'
? courseDetail.courseProgress?.displayOrder == 1
? Container()
: _buildTileShaderWrapper()
: Container();
Widget _buildTileShaderWrapper() => Positioned.fill(
child: _buildTileShader(),
);
Widget _buildTileShader() => Container(
decoration: BoxDecoration(
color: kcWhite.withOpacity(0.5),
borderRadius: BorderRadius.circular(5),
),
);
}

View File

@ -59,9 +59,9 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
trailing: _buildIconState(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
leading: _buildLeadingWrapper(),
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
leading: _buildLeadingWrapper(viewModel),
enabled: (lesson.access?.isAccessible ?? false),
controlAffinity: ListTileControlAffinity.trailing,
expandedCrossAxisAlignment: CrossAxisAlignment.start,
@ -78,10 +78,8 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
children: _buildExpansionTileChildren(viewModel),
);
Widget _buildLeadingWrapper() => MiniThumbnail(
thumbnail:
getReadableUrl(lesson.thumbnail ?? 'assets/images/image_1.png') ??
'assets/images/image_1.png');
Widget _buildLeadingWrapper(LearnLessonViewModel viewModel) => MiniThumbnail(
thumbnail: getReadableUrl(lesson.thumbnail ?? '') ?? '');
Widget _buildTitle() => Text(
lesson.title ?? '',

View File

@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
@ -9,6 +10,7 @@ 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/helper_functions.dart';
import '../common/ui_helpers.dart';
import 'custom_elevated_button.dart';
@ -63,10 +65,10 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
subtitle: _buildContent(),
trailing: _buildLockIcon(),
title: _buildTitleWrapper(),
leading: _buildIconWrapper(),
collapsedIconColor: kcDarkGrey,
collapsedTextColor: kcDarkGrey,
backgroundColor: kcBackgroundColor,
leading: _buildIconWrapper(viewModel),
shape: Border.all(color: kcTransparent),
expandedAlignment: Alignment.centerLeft,
collapsedBackgroundColor: kcBackgroundColor,
@ -87,16 +89,25 @@ class LearnModuleTile extends ViewModelWidget<LearnModuleViewModel> {
color: kcLightGrey,
);
Widget _buildIconWrapper() => CircleAvatar(
Widget _buildIconWrapper(LearnModuleViewModel viewModel) => CircleAvatar(
backgroundColor: kcPrimaryColor.withOpacity(0.1),
child: _buildIcon(),
child: _buildIconClipper(viewModel),
);
Widget _buildIcon() => const Icon(
Icons.lightbulb_outline,
color: kcPrimaryColor,
Widget _buildIconClipper(LearnModuleViewModel viewModel)=> ClipRRect(
child: _buildIcon(viewModel),
);
Widget _buildIcon(LearnModuleViewModel viewModel) =>
CachedNetworkImage(
width: 25,
height: 25,
cacheKey: viewModel.getModuleImage(module),
imageUrl: viewModel.getModuleImage(module),
);
Widget _buildTitleWrapper() => Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: _buildTitle(),

View File

@ -1,5 +1,5 @@
name: yimaru_app
version: 0.1.23+25
version: 0.1.24+26
publish_to: 'none'
description: A new Flutter project.