Compare commits

...

3 Commits

Author SHA1 Message Date
0824019612 Merge branch 'release/0.1.36'
-fix(landing): Change landing image pictures for quality improvement
2026-06-04 12:48:49 +03:00
da7fac2b12 fix(landing): Change landing image pictures for quality improvement 2026-06-04 12:47:43 +03:00
0a20c639f0 Merge tag '0.1.35' into develop
-fix(subscription): Subscription logic updated
2026-06-03 04:02:06 +03:00
67 changed files with 2151 additions and 695 deletions

View File

@ -0,0 +1,24 @@
<svg width="524" height="173" viewBox="0 0 524 173" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_23_9019)">
<path d="M194.31 28.74L206.48 51L219.27 28.76H232.21L212.29 62.86V83H200.67V62.84L180.75 28.74H194.31Z" fill="#9E2891"/>
<path d="M247.79 32.69C247.809 33.51 247.661 34.3252 247.353 35.0855C247.045 35.8457 246.584 36.5347 246 37.11C245.412 37.6962 244.71 38.1564 243.938 38.4625C243.166 38.7685 242.34 38.914 241.51 38.89C240.677 38.907 239.85 38.7584 239.076 38.4528C238.302 38.1472 237.596 37.6908 237 37.11C236.41 36.5373 235.944 35.8494 235.631 35.0891C235.317 34.3287 235.164 33.5121 235.18 32.69C235.159 31.8731 235.31 31.0608 235.623 30.3062C235.937 29.5515 236.406 28.8714 237 28.31C237.604 27.735 238.317 27.2853 239.096 26.9867C239.875 26.6881 240.706 26.5465 241.54 26.57C242.362 26.5477 243.18 26.6902 243.946 26.989C244.712 27.2878 245.41 27.737 246 28.31C246.586 28.8755 247.048 29.5571 247.356 30.3113C247.665 31.0655 247.812 31.8756 247.79 32.69ZM246.93 43.15V83H236V43.15H246.93Z" fill="#9E2891"/>
<path d="M316.92 61.13V83.07H306V60.74C306 54.7 303.7 51.68 299.1 51.68C298.017 51.6409 296.939 51.8473 295.947 52.2837C294.954 52.7201 294.074 53.3752 293.37 54.2C291.819 56.1914 291.048 58.6803 291.2 61.2V83.07H280.26V60.74C280.26 54.7 277.927 51.68 273.26 51.68C272.176 51.6412 271.098 51.855 270.11 52.3045C269.123 52.754 268.253 53.4268 267.57 54.27C266.061 56.2815 265.304 58.7583 265.43 61.27V83.07H254.53V43.15H264.06L265.06 48.15C266.341 46.4745 267.981 45.1076 269.86 44.15C272.055 43.1452 274.447 42.643 276.86 42.68C282.54 42.68 286.494 44.9267 288.72 49.42C290.134 47.2728 292.099 45.5457 294.41 44.42C296.907 43.2298 299.645 42.6342 302.41 42.68C304.364 42.6151 306.308 42.9667 308.115 43.7116C309.922 44.4564 311.55 45.5774 312.89 47C315.57 49.8067 316.914 54.5167 316.92 61.13Z" fill="#9E2891"/>
<path d="M363 83H357.42C352.4 83 349.944 80.83 350.05 76.49C348.731 78.6384 346.895 80.4226 344.71 81.68C342.347 82.9271 339.701 83.5403 337.03 83.46C332.59 83.46 329.014 82.44 326.3 80.4C324.968 79.4019 323.9 78.0925 323.19 76.5864C322.481 75.0802 322.151 73.4231 322.23 71.76C322.153 69.8652 322.549 67.9809 323.382 66.2771C324.214 64.5733 325.458 63.1034 327 62C330.12 59.7 334.654 58.55 340.6 58.55H348.51V56.56C348.542 55.736 348.377 54.9164 348.029 54.1687C347.681 53.4211 347.161 52.7668 346.51 52.26C344.913 51.1085 342.966 50.5465 341 50.67C339.238 50.5873 337.492 51.0481 336 51.99C335.383 52.3861 334.857 52.9079 334.455 53.5213C334.054 54.1348 333.786 54.8261 333.67 55.55H323.19C323.3 53.6561 323.848 51.8134 324.79 50.1668C325.732 48.5201 327.043 47.1144 328.62 46.06C331.874 43.8133 336.187 42.69 341.56 42.69C347.187 42.69 351.54 43.93 354.62 46.41C357.7 48.89 359.237 52.48 359.23 57.18V71.18C359.19 71.5381 359.224 71.9007 359.331 72.2449C359.438 72.5891 359.614 72.9075 359.85 73.18C360.408 73.5936 361.098 73.7892 361.79 73.73H363V83ZM340.47 65.78C338.602 65.6742 336.747 66.1493 335.16 67.14C334.544 67.5708 334.048 68.1509 333.717 68.8261C333.386 69.5012 333.233 70.2492 333.27 71C333.247 71.6411 333.379 72.2783 333.654 72.8578C333.929 73.4374 334.339 73.9423 334.85 74.33C336.106 75.2174 337.625 75.6544 339.16 75.57C340.39 75.6344 341.62 75.4517 342.777 75.0326C343.935 74.6135 344.997 73.9666 345.9 73.13C346.741 72.2633 347.401 71.2376 347.84 70.1129C348.28 68.9882 348.49 67.7872 348.46 66.58V65.8L340.47 65.78Z" fill="#9E2891"/>
<path d="M391.54 53.07H387.2C384.1 53.07 381.827 54 380.38 55.86C378.827 58.0793 378.063 60.7553 378.21 63.46V83H367.29V43.15H377.21L378.21 49.15C379.277 47.3096 380.799 45.7739 382.63 44.69C384.812 43.5902 387.239 43.0702 389.68 43.18H391.54V53.07Z" fill="#9E2891"/>
<path d="M433.94 83H424.25L423.25 78.19C421.853 79.9282 420.065 81.3117 418.032 82.2275C415.999 83.1434 413.778 83.5657 411.55 83.46C409.459 83.5413 407.373 83.2027 405.415 82.4641C403.457 81.7256 401.667 80.6021 400.15 79.16C397.21 76.2867 395.74 71.4967 395.74 64.79V43.15H406.66V63.46C406.66 67.08 407.3 69.8 408.56 71.63C409.218 72.552 410.101 73.2899 411.125 73.7731C412.149 74.2564 413.28 74.4687 414.41 74.39C415.642 74.4479 416.869 74.1975 417.979 73.6614C419.09 73.1253 420.049 72.3205 420.77 71.32C422.257 69.28 423 66.4267 423 62.76V43.15H433.93L433.94 83Z" fill="#9E2891"/>
<path d="M200.9 89.43H214.85L234.54 143.68H222.06L217.87 131.82H197.26L193.07 143.68H181.07L200.9 89.43ZM214.7 122.43L207.49 101.82L200.36 122.43H214.7Z" fill="#9E2891"/>
<path d="M255.77 144.15C251.999 144.235 248.268 143.369 244.92 141.63C241.873 140.003 239.377 137.507 237.75 134.46C236.09 131.132 235.226 127.464 235.226 123.745C235.226 120.026 236.09 116.358 237.75 113.03C239.403 109.984 241.927 107.501 245 105.9C248.362 104.162 252.107 103.296 255.89 103.38C261.224 103.38 265.557 104.737 268.89 107.45C272.315 110.281 274.532 114.312 275.09 118.72H263.75C263.384 116.884 262.417 115.223 261 114C259.536 112.814 257.693 112.197 255.81 112.26C254.504 112.217 253.205 112.482 252.02 113.035C250.835 113.587 249.797 114.411 248.99 115.44C247.323 117.885 246.431 120.776 246.431 123.735C246.431 126.694 247.323 129.585 248.99 132.03C249.799 133.056 250.838 133.877 252.023 134.427C253.207 134.978 254.505 135.243 255.81 135.2C257.737 135.266 259.623 134.634 261.12 133.42C262.579 132.164 263.552 130.438 263.87 128.54H275.11C274.597 133.016 272.376 137.121 268.91 140C265.517 142.76 261.137 144.143 255.77 144.15Z" fill="#9E2891"/>
<path d="M320.17 143.68H314.59C309.583 143.68 307.13 141.51 307.23 137.17C305.907 139.319 304.068 141.103 301.88 142.36C299.521 143.609 296.878 144.226 294.21 144.15C289.76 144.15 286.21 143.15 283.47 141.08C282.137 140.082 281.069 138.773 280.36 137.266C279.65 135.76 279.321 134.103 279.4 132.44C279.31 130.546 279.691 128.659 280.51 126.949C281.328 125.239 282.558 123.758 284.09 122.64C287.223 120.34 291.76 119.19 297.7 119.19H305.6V117.25C305.632 116.426 305.467 115.606 305.119 114.858C304.772 114.11 304.251 113.456 303.6 112.95C302.059 111.957 300.275 111.407 298.442 111.359C296.61 111.312 294.8 111.768 293.21 112.68C292.595 113.078 292.071 113.6 291.671 114.213C291.271 114.827 291.005 115.517 290.89 116.24H280.33C280.442 114.347 280.99 112.505 281.932 110.858C282.874 109.212 284.184 107.806 285.76 106.75C289.013 104.503 293.326 103.38 298.7 103.38C304.34 103.38 308.693 104.62 311.76 107.1C314.826 109.58 316.363 113.17 316.37 117.87V131.87C316.332 132.228 316.367 132.59 316.474 132.934C316.58 133.278 316.756 133.596 316.99 133.87C317.552 134.274 318.239 134.466 318.93 134.41H320.17V143.68ZM297.62 126.47C295.751 126.36 293.895 126.836 292.31 127.83C291.694 128.257 291.197 128.833 290.865 129.505C290.532 130.177 290.376 130.921 290.41 131.67C290.388 132.312 290.521 132.95 290.798 133.529C291.074 134.109 291.487 134.613 292 135C293.252 135.887 294.767 136.324 296.3 136.24C297.523 136.301 298.746 136.117 299.897 135.698C301.047 135.279 302.102 134.634 303 133.8C303.844 132.936 304.506 131.911 304.946 130.785C305.386 129.66 305.594 128.458 305.56 127.25V126.47H297.62Z" fill="#9E2891"/>
<path d="M364.27 143.68H354.73L353.65 138.18C352.225 140.111 350.352 141.666 348.191 142.712C346.031 143.757 343.649 144.261 341.25 144.18C337.925 144.23 334.65 143.358 331.79 141.66C328.968 139.944 326.695 137.456 325.24 134.49C323.61 131.134 322.802 127.44 322.88 123.71C322.795 120.002 323.604 116.329 325.24 113C326.711 110.043 328.998 107.569 331.83 105.87C334.706 104.174 337.992 103.302 341.33 103.35C346.49 103.35 350.49 105.157 353.33 108.77V89.43H364.26L364.27 143.68ZM353.5 123.84C353.645 120.825 352.708 117.857 350.86 115.47C350.01 114.432 348.931 113.605 347.708 113.054C346.485 112.502 345.151 112.241 343.81 112.29C342.464 112.241 341.124 112.503 339.895 113.054C338.665 113.605 337.579 114.432 336.72 115.47C334.978 117.885 334.041 120.787 334.041 123.765C334.041 126.743 334.978 129.645 336.72 132.06C337.581 133.095 338.668 133.919 339.897 134.468C341.126 135.018 342.465 135.278 343.81 135.23C345.147 135.283 346.479 135.027 347.701 134.482C348.924 133.938 350.005 133.119 350.86 132.09C352.705 129.748 353.642 126.818 353.5 123.84Z" fill="#9E2891"/>
<path d="M379.88 105.9C383.105 104.169 386.722 103.301 390.38 103.38C394.058 103.311 397.698 104.12 401 105.74C403.986 107.237 406.486 109.55 408.21 112.41C410.559 116.613 411.392 121.496 410.57 126.24H381.7V126.55C381.805 129.079 382.792 131.492 384.49 133.37C385.356 134.219 386.389 134.878 387.524 135.305C388.658 135.732 389.87 135.917 391.08 135.85C393.027 135.923 394.952 135.426 396.62 134.42C398.103 133.446 399.147 131.932 399.53 130.2H410.3C409.913 132.787 408.881 135.236 407.3 137.32C405.634 139.494 403.446 141.214 400.94 142.32C398.09 143.585 394.998 144.21 391.88 144.15C387.901 144.251 383.955 143.401 380.37 141.67C377.234 140.103 374.642 137.629 372.93 134.57C371.14 131.282 370.243 127.582 370.33 123.84C370.23 120.073 371.09 116.342 372.83 113C374.447 110.003 376.895 107.538 379.88 105.9ZM397 113.49C395.244 112.124 393.064 111.416 390.84 111.49C388.664 111.409 386.532 112.119 384.84 113.49C383.196 114.894 382.157 116.879 381.94 119.03H400C399.746 116.868 398.672 114.885 397 113.49Z" fill="#9E2891"/>
<path d="M479.28 121.82V143.76H468.35V121.44C468.35 115.393 466.05 112.37 461.45 112.37C460.366 112.329 459.288 112.535 458.295 112.972C457.303 113.408 456.422 114.064 455.72 114.89C454.167 116.881 453.392 119.369 453.54 121.89V143.75H442.62V121.44C442.62 115.393 440.286 112.37 435.62 112.37C434.534 112.33 433.453 112.543 432.464 112.992C431.475 113.442 430.604 114.115 429.92 114.96C428.413 116.973 427.659 119.449 427.79 121.96V143.74H416.89V103.84H426.42L427.42 108.84C428.7 107.164 430.341 105.798 432.22 104.84C434.415 103.835 436.806 103.333 439.22 103.37C444.9 103.37 448.85 105.617 451.07 110.11C452.49 107.965 454.458 106.238 456.77 105.11C459.266 103.92 462.004 103.324 464.77 103.37C466.717 103.299 468.657 103.642 470.462 104.376C472.267 105.111 473.895 106.22 475.24 107.63C477.926 110.477 479.273 115.207 479.28 121.82Z" fill="#9E2891"/>
<path d="M502.21 132.44L511.67 103.84H523.37L505.7 149.34C504.985 151.312 504.102 153.219 503.06 155.04C502.312 156.331 501.231 157.398 499.93 158.13C498.401 158.889 496.706 159.25 495 159.18H484.39V150H490.39C491.439 150.081 492.486 149.836 493.39 149.3C494.197 148.556 494.808 147.625 495.17 146.59L496.17 143.95L480.9 103.84H492.52L502.21 132.44Z" fill="#9E2891"/>
<path d="M121.07 26.53L98.76 39.42L76.03 52.54L53.26 39.39L53.24 39.38L30.96 26.52L31.41 25.76L53.24 13.15L53.26 13.13L76.02 0L98.76 13.12L120.6 25.74L121.07 26.53Z" fill="#9E2891"/>
<path d="M53.26 172.41H98.76V110.9L121.51 97.78L137.93 88.29L152.02 80.15L129.27 40.75L98.76 58.36L92.43 62.02L76 71.49L59.6 62.03L53.26 58.37L53.24 58.36L22.75 40.75L0 80.16L14.09 88.3L30.5 97.78L53.24 110.9L53.26 110.91V137.18V172.41Z" fill="#9E2891"/>
</g>
<defs>
<clipPath id="clip0_23_9019">
<rect width="523.37" height="172.42" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/images/landing_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/images/landing_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/images/landing_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

View File

@ -193,7 +193,8 @@
"keep_momentum":"በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።", "keep_momentum":"በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።",
"completed_practices": "የተጠናቀቁ ልምምዶች", "completed_practices": "የተጠናቀቁ ልምምዶች",
"total_practices": "ጠቅላላ ልምምዶች", "total_practices": "ጠቅላላ ልምምዶች",
"progress_percentage": "የእድገት መቶኛ" "progress_percentage": "የእድገት መቶኛ",
"notifications": "ማሳወቂያዎች"
} }

View File

@ -193,5 +193,6 @@
"keep_momentum":"Great job! Keep the momentum.", "keep_momentum":"Great job! Keep the momentum.",
"completed_practices": "Completed Practices", "completed_practices": "Completed Practices",
"total_practices": "Total Practices", "total_practices": "Total Practices",
"progress_percentage": "Progress Percentage" "progress_percentage": "Progress Percentage",
"notifications": "Notifications"
} }

View File

@ -35,7 +35,6 @@ import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart'; import 'package:yimaru_app/ui/views/course_payment/course_payment_view.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart'; import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart'; import 'package:yimaru_app/ui/views/course_lesson_detail/course_lesson_detail_view.dart';
import 'package:yimaru_app/services/notification_service.dart';
import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart'; import 'package:yimaru_app/ui/views/duolingo/duolingo_view.dart';
import 'package:yimaru_app/services/smart_auth_service.dart'; import 'package:yimaru_app/services/smart_auth_service.dart';
import 'package:yimaru_app/services/course_service.dart'; import 'package:yimaru_app/services/course_service.dart';
@ -58,6 +57,9 @@ import 'package:yimaru_app/ui/views/course_module/course_module_view.dart';
import 'package:yimaru_app/services/onboarding_service.dart'; import 'package:yimaru_app/services/onboarding_service.dart';
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart'; import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
import 'package:yimaru_app/ui/views/payment/payment_view.dart'; import 'package:yimaru_app/ui/views/payment/payment_view.dart';
import 'package:yimaru_app/ui/views/notification/notification_view.dart';
import 'package:yimaru_app/services/in_app_notification_service.dart';
import 'package:yimaru_app/services/push_notification_service.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -97,6 +99,7 @@ import 'package:yimaru_app/ui/views/payment/payment_view.dart';
MaterialRoute(page: CourseModuleView), MaterialRoute(page: CourseModuleView),
MaterialRoute(page: LearnCourseView), MaterialRoute(page: LearnCourseView),
MaterialRoute(page: PaymentView), MaterialRoute(page: PaymentView),
MaterialRoute(page: NotificationView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [
@ -112,7 +115,6 @@ import 'package:yimaru_app/ui/views/payment/payment_view.dart';
LazySingleton(classType: ImagePickerService), LazySingleton(classType: ImagePickerService),
LazySingleton(classType: GoogleAuthService), LazySingleton(classType: GoogleAuthService),
LazySingleton(classType: ImageDownloaderService), LazySingleton(classType: ImageDownloaderService),
LazySingleton(classType: NotificationService),
LazySingleton(classType: SmartAuthService), LazySingleton(classType: SmartAuthService),
LazySingleton(classType: CourseService), LazySingleton(classType: CourseService),
LazySingleton(classType: AudioPlayerService), LazySingleton(classType: AudioPlayerService),
@ -124,6 +126,8 @@ import 'package:yimaru_app/ui/views/payment/payment_view.dart';
LazySingleton(classType: LearnService), LazySingleton(classType: LearnService),
LazySingleton(classType: LocalizationService), LazySingleton(classType: LocalizationService),
LazySingleton(classType: OnboardingService), LazySingleton(classType: OnboardingService),
LazySingleton(classType: InAppNotificationService),
LazySingleton(classType: PushNotificationService),
// @stacked-service // @stacked-service
], ],
bottomsheets: [ bottomsheets: [

View File

@ -20,13 +20,14 @@ import '../services/dio_service.dart';
import '../services/google_auth_service.dart'; import '../services/google_auth_service.dart';
import '../services/image_downloader_service.dart'; import '../services/image_downloader_service.dart';
import '../services/image_picker_service.dart'; import '../services/image_picker_service.dart';
import '../services/in_app_notification_service.dart';
import '../services/in_app_update_service.dart'; import '../services/in_app_update_service.dart';
import '../services/learn_service.dart'; import '../services/learn_service.dart';
import '../services/localization_service.dart'; import '../services/localization_service.dart';
import '../services/notification_service.dart';
import '../services/onboarding_service.dart'; import '../services/onboarding_service.dart';
import '../services/permission_handler_service.dart'; import '../services/permission_handler_service.dart';
import '../services/phone_caller_service.dart'; import '../services/phone_caller_service.dart';
import '../services/push_notification_service.dart';
import '../services/secure_storage_service.dart'; import '../services/secure_storage_service.dart';
import '../services/smart_auth_service.dart'; import '../services/smart_auth_service.dart';
import '../services/status_checker_service.dart'; import '../services/status_checker_service.dart';
@ -55,7 +56,6 @@ Future<void> setupLocator(
locator.registerLazySingleton(() => ImagePickerService()); locator.registerLazySingleton(() => ImagePickerService());
locator.registerLazySingleton(() => GoogleAuthService()); locator.registerLazySingleton(() => GoogleAuthService());
locator.registerLazySingleton(() => ImageDownloaderService()); locator.registerLazySingleton(() => ImageDownloaderService());
locator.registerLazySingleton(() => NotificationService());
locator.registerLazySingleton(() => SmartAuthService()); locator.registerLazySingleton(() => SmartAuthService());
locator.registerLazySingleton(() => CourseService()); locator.registerLazySingleton(() => CourseService());
locator.registerLazySingleton(() => AudioPlayerService()); locator.registerLazySingleton(() => AudioPlayerService());
@ -67,4 +67,6 @@ Future<void> setupLocator(
locator.registerLazySingleton(() => LearnService()); locator.registerLazySingleton(() => LearnService());
locator.registerLazySingleton(() => LocalizationService()); locator.registerLazySingleton(() => LocalizationService());
locator.registerLazySingleton(() => OnboardingService()); locator.registerLazySingleton(() => OnboardingService());
locator.registerLazySingleton(() => InAppNotificationService());
locator.registerLazySingleton(() => PushNotificationService());
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,8 @@ import 'package:yimaru_app/app/app.dialogs.dart';
import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/services/notification_service.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:yimaru_app/services/push_notification_service.dart';
import 'package:yimaru_app/ui/common/translations/codegen_loader.g.dart'; import 'package:yimaru_app/ui/common/translations/codegen_loader.g.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
@ -17,7 +17,7 @@ Future<void> main() async {
await setupLocator(); await setupLocator();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await locator<NotificationService>().initialize(); await locator<PushNotificationService>().initialize();
await EasyLocalization.ensureInitialized(); await EasyLocalization.ensureInitialized();
setupDialogUi(); setupDialogUi();
setupBottomSheetUi(); setupBottomSheetUi();

View File

@ -0,0 +1,57 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/notification_payload.dart';
part 'in_app_notification.g.dart';
@JsonSerializable()
class InAppNotification {
final String? id;
final String? type;
final String? level;
final String? image;
final NotificationPayload? payload;
@JsonKey(name: 'is_read')
final bool? isRead;
@JsonKey(name: 'reciever')
final String? receiver;
@JsonKey(name: 'recipient_id')
final int? recipientId;
@JsonKey(name: 'receiver_type')
final String? receiverType;
@JsonKey(name: 'error_severity')
final String? errorSeverity;
@JsonKey(name: 'delivery_status')
final String? deliveryStatus;
@JsonKey(name: 'delivery_channel')
final String? deliveryChannel;
const InAppNotification(
{this.id,
this.type,
this.image,
this.level,
this.isRead,
this.payload,
this.receiver,
this.recipientId,
this.receiverType,
this.errorSeverity,
this.deliveryStatus,
this.deliveryChannel});
factory InAppNotification.fromJson(Map<String, dynamic> json) =>
_$InAppNotificationFromJson(json);
Map<String, dynamic> toJson() => _$InAppNotificationToJson(this);
}

View File

@ -0,0 +1,42 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'in_app_notification.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
InAppNotification _$InAppNotificationFromJson(Map<String, dynamic> json) =>
InAppNotification(
id: json['id'] as String?,
type: json['type'] as String?,
image: json['image'] as String?,
level: json['level'] as String?,
isRead: json['is_read'] as bool?,
payload: json['payload'] == null
? null
: NotificationPayload.fromJson(
json['payload'] as Map<String, dynamic>),
receiver: json['reciever'] as String?,
recipientId: (json['recipient_id'] as num?)?.toInt(),
receiverType: json['receiver_type'] as String?,
errorSeverity: json['error_severity'] as String?,
deliveryStatus: json['delivery_status'] as String?,
deliveryChannel: json['delivery_channel'] as String?,
);
Map<String, dynamic> _$InAppNotificationToJson(InAppNotification instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'level': instance.level,
'image': instance.image,
'payload': instance.payload,
'is_read': instance.isRead,
'reciever': instance.receiver,
'recipient_id': instance.recipientId,
'receiver_type': instance.receiverType,
'error_severity': instance.errorSeverity,
'delivery_status': instance.deliveryStatus,
'delivery_channel': instance.deliveryChannel,
};

View File

@ -0,0 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
part 'notification_payload.g.dart';
@JsonSerializable()
class NotificationPayload {
final String? message;
final String? headline;
const NotificationPayload({this.message, this.headline});
factory NotificationPayload.fromJson(Map<String, dynamic> json) =>
_$NotificationPayloadFromJson(json);
Map<String, dynamic> toJson() => _$NotificationPayloadToJson(this);
}

View File

@ -0,0 +1,20 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification_payload.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NotificationPayload _$NotificationPayloadFromJson(Map<String, dynamic> json) =>
NotificationPayload(
message: json['message'] as String?,
headline: json['headline'] as String?,
);
Map<String, dynamic> _$NotificationPayloadToJson(
NotificationPayload instance) =>
<String, dynamic>{
'message': instance.message,
'headline': instance.headline,
};

View File

@ -1,5 +1,6 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:yimaru_app/models/app_update.dart'; import 'package:yimaru_app/models/app_update.dart';
import 'package:yimaru_app/models/in_app_notification.dart';
import 'package:yimaru_app/models/learn_lesson.dart'; import 'package:yimaru_app/models/learn_lesson.dart';
import 'package:yimaru_app/models/learn_practice.dart'; import 'package:yimaru_app/models/learn_practice.dart';
import 'package:yimaru_app/models/learn_program.dart'; import 'package:yimaru_app/models/learn_program.dart';
@ -355,6 +356,55 @@ class ApiService {
} }
} }
// Mark notifications
Future<void> markNotificationsRead() async {
try {
await _service.dio.post(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kNotificationsUrl/$kMarkNotificationRead');
} catch (e) {
return;
}
}
// Get unread notifications
Future<int> getUnreadNotifications() async {
try {
final Response response = await _service.dio.get(
'$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kNotificationsUrl/$kUnreadUrl');
if (response.statusCode == 200) {
return response.data['unread'];
}
return 0;
} catch (e) {
return 0;
}
}
// Get notifications
Future<List<InAppNotification>> getAllNotifications() async {
try {
List<InAppNotification> notifications = [];
final Response response = await _service.dio
.get('$kBaseUrl/$kApiUrl/$kApiVersionUrl/$kNotificationsUrl');
if (response.statusCode == 200) {
var data = response.data;
var decodedData = data['notifications'] as List;
notifications = decodedData.map(
(e) {
return InAppNotification.fromJson(e);
},
).toList();
return notifications;
}
return [];
} catch (e) {
return [];
}
}
// Get assessment question sets // Get assessment question sets
Future<List<Assessment>> getAssessments() async { Future<List<Assessment>> getAssessments() async {
try { try {

View File

@ -0,0 +1,44 @@
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/models/in_app_notification.dart';
import '../app/app.locator.dart';
import 'api_service.dart';
class InAppNotificationService with ListenableServiceMixin {
// Dependency injection
final _apiService = locator<ApiService>();
// Initialization
learnService() {
listenToReactiveValues([_unreadCount, _notifications]);
}
// Unread count
int _unreadCount = 0;
int get unreadCount => _unreadCount;
// Notifications
List<InAppNotification> _notifications = [];
List<InAppNotification> get notifications => _notifications;
// Unread notifications
Future<void> markNotificationRead() async {
await _apiService.markNotificationsRead();
_unreadCount = await _apiService.getUnreadNotifications();
notifyListeners();
}
// All notifications
Future<void> getAllNotifications() async {
_notifications = await _apiService.getAllNotifications();
notifyListeners();
}
// Unread notifications
Future<void> getUnreadNotifications() async {
_unreadCount = await _apiService.getUnreadNotifications();
notifyListeners();
}
}

View File

@ -1,138 +0,0 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:yimaru_app/app/app.locator.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await locator<NotificationService>().setupFlutterNotifications();
await locator<NotificationService>().showNotification(message);
}
class NotificationService {
final _messaging = FirebaseMessaging.instance;
bool _isFlutterLocalNotificationInitialized = false;
final _localNotifications = FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
// Initialize FCM token
await updateFCMToken();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Request permission
await _requestPermission();
// setup message handle
await _setupMessageHandler();
// Subscribe to all devices
subscribeToTopic('yimaru');
}
Future<void> _requestPermission() async {
await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
carPlay: false,
provisional: false,
announcement: false,
criticalAlert: false);
}
Future<void> setupFlutterNotifications() async {
if (_isFlutterLocalNotificationInitialized) {
return;
}
// Android setup
const channel = AndroidNotificationChannel(
'yimaru', // id
'Yimaru', // title
importance: Importance.high,
);
await _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
const initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
// IOS setup
const initializationSettingsDarwin = DarwinInitializationSettings();
const initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin);
// Flutter notification setup
await _localNotifications.initialize(
settings: initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
if (response.payload == 'Page') {
// navigatorKey.currentState?.pushNamed('RouteName');
}
},
);
_isFlutterLocalNotificationInitialized = true;
}
Future<void> showNotification(RemoteMessage message) async {
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
if (notification != null && android != null) {
await _localNotifications.show(
id: notification.hashCode,
title: notification.title,
body: notification.body,
notificationDetails: const NotificationDetails(
android: AndroidNotificationDetails('yimaru', 'Yimaru',
enableVibration: true,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
importance: Importance.high),
iOS: DarwinNotificationDetails(
presentAlert: true, presentBadge: true, presentSound: true)),
);
}
}
Future<void> _setupMessageHandler() async {
// Foreground message
FirebaseMessaging.onMessage
.listen((RemoteMessage message) => showNotification(message));
// Background message
FirebaseMessaging.onMessageOpenedApp.listen(_handleBackgroundMessage);
// Opened app
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
_handleBackgroundMessage(initialMessage);
}
}
void _handleBackgroundMessage(RemoteMessage message) {
if (message.data['type'] == 'Page') {
// navigatorKey.currentState?.pushNamed('RouteName');
}
}
Future<void> subscribeToTopic(String topic) async {
await FirebaseMessaging.instance.subscribeToTopic(topic);
}
Future<void> updateFCMToken() async {
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
_messaging.onTokenRefresh.listen((newToken) {
// updateTokenOnServer(newToken);
});
}
}

View File

@ -0,0 +1,136 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:yimaru_app/app/app.locator.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await locator<PushNotificationService>().setupFlutterNotifications();
await locator<PushNotificationService>().showNotification(message);
}
class PushNotificationService { final _messaging = FirebaseMessaging.instance;
bool _isFlutterLocalNotificationInitialized = false;
final _localNotifications = FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
// Initialize FCM token
await updateFCMToken();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Request permission
await _requestPermission();
// setup message handle
await _setupMessageHandler();
// Subscribe to all devices
subscribeToTopic('yimaru');
}
Future<void> _requestPermission() async {
await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
carPlay: false,
provisional: false,
announcement: false,
criticalAlert: false);
}
Future<void> setupFlutterNotifications() async {
if (_isFlutterLocalNotificationInitialized) {
return;
}
// Android setup
const channel = AndroidNotificationChannel(
'yimaru', // id
'Yimaru', // title
importance: Importance.high,
);
await _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
const initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
// IOS setup
const initializationSettingsDarwin = DarwinInitializationSettings();
const initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin);
// Flutter notification setup
await _localNotifications.initialize(
settings: initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
if (response.payload == 'Page') {
// navigatorKey.currentState?.pushNamed('RouteName');
}
},
);
_isFlutterLocalNotificationInitialized = true;
}
Future<void> showNotification(RemoteMessage message) async {
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
if (notification != null && android != null) {
await _localNotifications.show(
id: notification.hashCode,
title: notification.title,
body: notification.body,
notificationDetails: const NotificationDetails(
android: AndroidNotificationDetails('yimaru', 'Yimaru',
enableVibration: true,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
importance: Importance.high),
iOS: DarwinNotificationDetails(
presentAlert: true, presentBadge: true, presentSound: true)),
);
}
}
Future<void> _setupMessageHandler() async {
// Foreground message
FirebaseMessaging.onMessage
.listen((RemoteMessage message) => showNotification(message));
// Background message
FirebaseMessaging.onMessageOpenedApp.listen(_handleBackgroundMessage);
// Opened app
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
_handleBackgroundMessage(initialMessage);
}
}
void _handleBackgroundMessage(RemoteMessage message) {
if (message.data['type'] == 'Page') {
// navigatorKey.currentState?.pushNamed('RouteName');
}
}
Future<void> subscribeToTopic(String topic) async {
await FirebaseMessaging.instance.subscribeToTopic(topic);
}
Future<void> updateFCMToken() async {
// print('DEVICE TOKEN: ${await _messaging.getToken()}');
_messaging.onTokenRefresh.listen((newToken) {
// updateTokenOnServer(newToken);
});
}
}

View File

@ -13,12 +13,15 @@ String kCheckUrl = 'check';
String kFilesUrl = 'files'; String kFilesUrl = 'files';
String kUnreadUrl = 'unread';
String kApiVersionUrl = 'v1'; String kApiVersionUrl = 'v1';
String kLevelsUrl = 'levels'; String kLevelsUrl = 'levels';
String kCoursesUrl = 'courses'; String kCoursesUrl = 'courses';
String kModulesUrl = 'modules'; String kModulesUrl = 'modules';
String kLessonsUrl = 'lessons'; String kLessonsUrl = 'lessons';
@ -69,6 +72,8 @@ String kQuestionSetsUrl = 'question-sets';
String kRequestResetCode = 'sendResetCode'; String kRequestResetCode = 'sendResetCode';
String kNotificationsUrl = 'notifications';
String kSubcategoriesUrl = 'sub-categories'; String kSubcategoriesUrl = 'sub-categories';
String kProgressSummary = 'progress-summary'; String kProgressSummary = 'progress-summary';
@ -79,6 +84,8 @@ String kCoursePracticeQuestions = 'questions';
String kCatalogCoursesUrl = 'catalog-courses'; String kCatalogCoursesUrl = 'catalog-courses';
String kMarkNotificationRead = 'mark-all-read';
String kUpdateProfileImage = 'profile-picture'; String kUpdateProfileImage = 'profile-picture';
String kSubscriptionsUrl = 'subscription-plans'; String kSubscriptionsUrl = 'subscription-plans';

View File

@ -50,6 +50,7 @@ enum StateObjects {
profileUpdate, profileUpdate,
resetPassword, resetPassword,
learnPractice, learnPractice,
notifications,
courseCatalogs, courseCatalogs,
loginWithEmail, loginWithEmail,
coursePractice, coursePractice,

View File

@ -213,7 +213,8 @@ class CodegenLoader extends AssetLoader {
"keep_momentum": "በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።", "keep_momentum": "በጣም ጥሩ ስራ! በዚሁ ብርታት ይቀጥሉ።",
"completed_practices": "የተጠናቀቁ ልምምዶች", "completed_practices": "የተጠናቀቁ ልምምዶች",
"total_practices": "ጠቅላላ ልምምዶች", "total_practices": "ጠቅላላ ልምምዶች",
"progress_percentage": "የእድገት መቶኛ" "progress_percentage": "የእድገት መቶኛ",
"notifications": "ማሳወቂያዎች"
}; };
static const Map<String, dynamic> _en = { static const Map<String, dynamic> _en = {
"loading": "Loading", "loading": "Loading",
@ -433,7 +434,8 @@ class CodegenLoader extends AssetLoader {
"keep_momentum": "Great job! Keep the momentum.", "keep_momentum": "Great job! Keep the momentum.",
"completed_practices": "Completed Practices", "completed_practices": "Completed Practices",
"total_practices": "Total Practices", "total_practices": "Total Practices",
"progress_percentage": "Progress Percentage" "progress_percentage": "Progress Percentage",
"notifications": "Notifications"
}; };
static const Map<String, Map<String, dynamic>> mapLocales = { static const Map<String, Map<String, dynamic>> mapLocales = {
"am": _am, "am": _am,

View File

@ -199,4 +199,5 @@ abstract class LocaleKeys {
static const completed_practices = 'completed_practices'; static const completed_practices = 'completed_practices';
static const total_practices = 'total_practices'; static const total_practices = 'total_practices';
static const progress_percentage = 'progress_percentage'; static const progress_percentage = 'progress_percentage';
static const notifications = 'notifications';
} }

View File

@ -333,6 +333,12 @@ TextStyle style14LG400 = const TextStyle(
color: kcLightGrey, color: kcLightGrey,
); );
TextStyle style12W600 = const TextStyle(
fontSize: 12,
color: kcWhite,
fontWeight: FontWeight.w600
);
TextStyle style14MG400 = const TextStyle( TextStyle style14MG400 = const TextStyle(
color: kcMediumGrey, color: kcMediumGrey,
); );

View File

@ -51,6 +51,8 @@ class CourseView extends StackedView<CourseViewModel> {
Widget _buildAppBar(CourseViewModel viewModel) => ProfileAppBar( Widget _buildAppBar(CourseViewModel viewModel) => ProfileAppBar(
name: viewModel.user?.firstName, name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture, profileImage: viewModel.user?.profilePicture,
unreadCount: viewModel.unreadCount.toString(),
onTap: () async => await viewModel.navigateToNotification(),
); );
Widget _buildCategoryColumnWrapper(CourseViewModel viewModel) => Widget _buildCategoryColumnWrapper(CourseViewModel viewModel) =>

View File

@ -5,6 +5,7 @@ import '../../../app/app.locator.dart';
import '../../../app/app.router.dart'; import '../../../app/app.router.dart';
import '../../../models/user.dart'; import '../../../models/user.dart';
import '../../../services/authentication_service.dart'; import '../../../services/authentication_service.dart';
import '../../../services/in_app_notification_service.dart';
class CourseViewModel extends ReactiveViewModel { class CourseViewModel extends ReactiveViewModel {
// Dependency injection // Dependency injection
@ -13,15 +14,23 @@ class CourseViewModel extends ReactiveViewModel {
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
final _inAppNotificationService = locator<InAppNotificationService>();
@override @override
List<ListenableServiceMixin> get listenableServices => List<ListenableServiceMixin> get listenableServices =>
[_authenticationService]; [_authenticationService,_inAppNotificationService];
// Current user // Current user
User? get _user => _authenticationService.user; User? get _user => _authenticationService.user;
User? get user => _user; User? get user => _user;
// Notification count
int get _unreadCount => _inAppNotificationService.unreadCount;
int get unreadCount => _unreadCount;
// Course // Course
final List<Map<String, dynamic>> _courses = [ final List<Map<String, dynamic>> _courses = [
{ {
@ -41,6 +50,11 @@ class CourseViewModel extends ReactiveViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToNotification() async =>
await _navigationService.navigateToNotificationView();
Future<void> navigateToCourseCatalog() async => Future<void> navigateToCourseCatalog() async =>
await _navigationService.navigateToCourseCatalogView(); await _navigationService.navigateToCourseCatalogView();
} }

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/failure/screens/first_failure_screen.dart';
import 'package:yimaru_app/ui/views/failure/screens/second_failure_screen.dart';
import 'package:yimaru_app/ui/views/failure/screens/third_failure_screen.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
@ -26,94 +30,38 @@ class FailureView extends StackedView<FailureViewModel> {
_buildScaffoldWrapper(); _buildScaffoldWrapper();
Widget _buildScaffoldWrapper() => Scaffold( Widget _buildScaffoldWrapper() => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcPrimaryColor,
body: _buildScaffold(), body: _buildStartupScreens(),
); );
Widget _buildScaffold() => Stack( Widget _buildStartupScreens() => FlutterCarousel(
children: _buildScaffoldChildren(), options: FlutterCarouselOptions(
autoPlay: true,
viewportFraction: 1,
showIndicator: false,
height: double.maxFinite,
),
items: _buildScreens(),
); );
List<Widget> _buildScaffoldChildren() => [ List<Widget> _buildScreens() => [
_buildBackground(), _buildFirstFailure(),
_buildColumn(), _buildSecondFailure(),
_buildThirdFailure(),
]; ];
Widget _buildBackground() => Image.asset( Widget _buildFirstFailure() => FirstFailureScreen(
'assets/images/loading.png', label: label,
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildColumnChildren(),
);
List<Widget> _buildColumnChildren() =>
[_buildIconWrapper(), _buildSafeWrapper()];
Widget _buildIconWrapper() => Padding(
padding: const EdgeInsets.only(top: 100),
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg', height: 50);
Widget _buildSafeWrapper() => SafeArea(child: _buildBottomSectionWrapper());
Widget _buildBottomSectionWrapper() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildBottomSectionColumn(),
);
Widget _buildBottomSectionColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildBottomSectionChildren(),
);
List<Widget> _buildBottomSectionChildren() => [
_buildLoadingTextWrapper(),
verticalSpaceSmall,
_buildRetryButtonWrapper()
];
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
];
Widget _buildLoadingText() =>
Text('$label ...', style: const TextStyle(color: kcWhite, fontSize: 16));
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildRetryButtonWrapper() => GestureDetector(
onTap: onTap, onTap: onTap,
child: _buildRetryButton(),
); );
Widget _buildRetryButton() => Text( Widget _buildSecondFailure() => SecondFailureScreen(
'Retry', label: label,
style: style16W600.copyWith( onTap: onTap,
fontStyle: FontStyle.italic, decoration: TextDecoration.underline), );
Widget _buildThirdFailure() => ThirdFailureScreen(
label: label,
onTap: onTap,
); );
} }

View File

@ -0,0 +1,165 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/failure/failure_viewmodel.dart';
import 'package:yimaru_app/ui/views/startup/startup_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../common/translations/locale_keys.g.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
class FirstFailureScreen extends ViewModelWidget<FailureViewModel> {
final String label;
final GestureTapCallback onTap;
const FirstFailureScreen({super.key,required this.onTap,required this.label});
@override
Widget build(BuildContext context, FailureViewModel viewModel) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper( ) => Scaffold(
backgroundColor: kcPrimaryColor,
body: _buildScaffoldPadding(),
);
Widget _buildScaffoldPadding( ) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(),
);
Widget _buildScaffold( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren( ) =>
[_buildUpperColumn(), _buildLowerColumnWrapper()];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo_white.svg',
height: 25,
);
Widget _buildLowerColumnWrapper( ) => Expanded(
child: _buildLowerColumn(),
);
Widget _buildLowerColumn( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(),
);
List<Widget> _buildLowerColumnChildren( ) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper()
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25W600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25W400,
),
TextSpan(
text: ' ሰዓት ',
style: style25W600,
),
TextSpan(
text: 'ይማሩ!',
style: style25W400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_1.png',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper( ) =>
SafeArea(child: _buildContinueButtonWrapper());
Widget _buildContinueButtonWrapper( ) => Align(
alignment: Alignment.bottomCenter,
child: _buildLoadingTextContainer(),
);
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
horizontalSpaceSmall,
_buildRetryButtonWrapper()
];
Widget _buildLoadingText() =>
Text('$label...', style: style16W600);
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildRetryButtonWrapper() => GestureDetector(
onTap: onTap,
child: _buildRetryButton(),
);
Widget _buildRetryButton() => Text(
'Retry',
style: style16W600.copyWith(
fontStyle: FontStyle.italic, decoration: TextDecoration.underline),
);
}

View File

@ -0,0 +1,161 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/failure/failure_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../common/translations/locale_keys.g.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
class SecondFailureScreen extends ViewModelWidget<FailureViewModel> {
final String label;
final GestureTapCallback onTap;
const SecondFailureScreen(
{super.key, required this.onTap, required this.label});
@override
Widget build(BuildContext context, FailureViewModel viewModel) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper() => Scaffold(
backgroundColor: Colors.amber,
body: _buildScaffoldPadding(),
);
Widget _buildScaffoldPadding() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(),
);
Widget _buildScaffold() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren() =>
[_buildUpperColumn(), _buildLowerColumnWrapper()];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo_purple.svg',
height: 25,
);
Widget _buildLowerColumnWrapper() => Expanded(
child: _buildLowerColumn(),
);
Widget _buildLowerColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(),
);
List<Widget> _buildLowerColumnChildren() => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper()
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25P600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25P400,
),
TextSpan(
text: ' እድሜ ',
style: style25P600,
),
TextSpan(
text: 'ይማሩ!',
style: style25P400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_2.png',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper() => SafeArea(child: _buildContinueButtonWrapper());
Widget _buildContinueButtonWrapper() => Align(
alignment: Alignment.bottomCenter,
child: _buildLoadingTextContainer(),
);
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
verticalSpaceSmall,
_buildRetryButtonWrapper()
];
Widget _buildLoadingText() => Text('$label...', style: style16P600);
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildRetryButtonWrapper() => GestureDetector(
onTap: onTap,
child: _buildRetryButton(),
);
Widget _buildRetryButton() => Text(
'Retry',
style: style16P600.copyWith(
fontStyle: FontStyle.italic, decoration: TextDecoration.underline),
);
}

View File

@ -0,0 +1,165 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/failure/failure_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../common/translations/locale_keys.g.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
class ThirdFailureScreen extends ViewModelWidget<FailureViewModel> {
final String label;
final GestureTapCallback onTap;
const ThirdFailureScreen(
{super.key, required this.onTap, required this.label});
@override
Widget build(BuildContext context, FailureViewModel viewModel) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper( ) => Scaffold(
backgroundColor:kcWhite,
body: _buildScaffoldPadding(),
);
Widget _buildScaffoldPadding( ) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(),
);
Widget _buildScaffold( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren( ) =>
[_buildUpperColumn(), _buildLowerColumnWrapper()];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo_purple.svg',
height: 25,
);
Widget _buildLowerColumnWrapper( ) => Expanded(
child: _buildLowerColumn(),
);
Widget _buildLowerColumn( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(),
);
List<Widget> _buildLowerColumnChildren( ) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper()
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25P600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25P400,
),
TextSpan(
text: ' ቦታ ',
style: style25P600,
),
TextSpan(
text: 'ይማሩ!',
style: style25P400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_3.png',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper( ) =>
SafeArea(child: _buildContinueButtonWrapper());
Widget _buildContinueButtonWrapper( ) => Align(
alignment: Alignment.bottomCenter,
child: _buildLoadingTextContainer(),
);
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
horizontalSpaceSmall,_buildRetryButtonWrapper()
];
Widget _buildLoadingText() =>
Text('$label...', style: style16P600);
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
Widget _buildRetryButtonWrapper() => GestureDetector(
onTap: onTap,
child: _buildRetryButton(),
);
Widget _buildRetryButton() => Text(
'Retry',
style: style16P600.copyWith(
fontStyle: FontStyle.italic, decoration: TextDecoration.underline),
);
}

View File

@ -17,6 +17,8 @@ class HomeView extends StackedView<HomeViewModel> {
@override @override
void onViewModelReady(HomeViewModel viewModel) async { void onViewModelReady(HomeViewModel viewModel) async {
await viewModel.inAppUpdate(); await viewModel.inAppUpdate();
await viewModel.getUnreadNotifications();
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }

View File

@ -8,6 +8,7 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../services/authentication_service.dart'; import '../../../services/authentication_service.dart';
import '../../../services/in_app_notification_service.dart';
import '../../../services/in_app_update_service.dart'; import '../../../services/in_app_update_service.dart';
class HomeViewModel extends ReactiveViewModel { class HomeViewModel extends ReactiveViewModel {
@ -22,9 +23,11 @@ class HomeViewModel extends ReactiveViewModel {
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
final _inAppNotificationService = locator<InAppNotificationService>();
@override @override
List<ListenableServiceMixin> get listenableServices => List<ListenableServiceMixin> get listenableServices =>
[_authenticationService]; [_authenticationService,_inAppNotificationService];
// Current user // Current user
User? get _user => _authenticationService.user; User? get _user => _authenticationService.user;
@ -63,4 +66,12 @@ class HomeViewModel extends ReactiveViewModel {
await _inAppUpdateService.checkForUpdate(); await _inAppUpdateService.checkForUpdate();
} }
} }
// Unread notifications
Future<void> getUnreadNotifications() async {
if (await _statusChecker.checkConnection()) {
await _inAppNotificationService.getUnreadNotifications();
}
}
} }

View File

@ -3,7 +3,6 @@ import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/views/landing/screens/first_landing_screen.dart'; import 'package:yimaru_app/ui/views/landing/screens/first_landing_screen.dart';
import 'package:yimaru_app/ui/views/landing/screens/fourth_landing_screen.dart';
import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart'; import 'package:yimaru_app/ui/views/landing/screens/second_landing_screen.dart';
import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart'; import 'package:yimaru_app/ui/views/landing/screens/third_landing_screen.dart';
@ -44,17 +43,15 @@ class LandingView extends StackedView<LandingViewModel> {
); );
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildFirstWelcome(), _buildFirstLanding(),
_buildSecondWelcome(), _buildSecondLanding(),
_buildThirdWelcome(), _buildThirdLanding(),
_buildFourthWelcome()
]; ];
Widget _buildFirstWelcome() => const FirstLandingScreen(); Widget _buildFirstLanding() => const FirstLandingScreen();
Widget _buildSecondWelcome() => const SecondLandingScreen(); Widget _buildSecondLanding() => const SecondLandingScreen();
Widget _buildThirdWelcome() => const ThirdLandingScreen(); Widget _buildThirdLanding() => const ThirdLandingScreen();
Widget _buildFourthWelcome() => const FourthLandingScreen();
} }

View File

@ -49,7 +49,7 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
); );
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo_white.svg',
height: 25, height: 25,
); );
@ -100,7 +100,7 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
); );
Widget _buildImage() => Image.asset( Widget _buildImage() => Image.asset(
'assets/images/landing_1.jpg', 'assets/images/landing_1.png',
fit: BoxFit.cover, fit: BoxFit.cover,
); );
@ -126,10 +126,10 @@ class FirstLandingScreen extends ViewModelWidget<LandingViewModel> {
Widget _buildContinueButton(LandingViewModel viewModel) => Widget _buildContinueButton(LandingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
text: 'Next',
borderRadius: 25, borderRadius: 25,
onTap: viewModel.next, text: 'Get Started',
backgroundColor: kcWhite, backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
onTap: () async => await viewModel.setFirstTimeInstall(),
); );
} }

View File

@ -1,136 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
import '../landing_viewmodel.dart';
class FourthLandingScreen extends ViewModelWidget<LandingViewModel> {
const FourthLandingScreen({super.key});
@override
Widget build(BuildContext context, LandingViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LandingViewModel viewModel) => Scaffold(
backgroundColor: Colors.amber,
body: _buildScaffoldPadding(viewModel),
);
Widget _buildScaffoldPadding(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(viewModel),
);
Widget _buildScaffold(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(LandingViewModel viewModel) =>
[_buildUpperColumn(), _buildLowerColumnWrapper(viewModel)];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
color: kcPrimaryColor,
height: 25,
);
Widget _buildLowerColumnWrapper(LandingViewModel viewModel) => Expanded(
child: _buildLowerColumn(viewModel),
);
Widget _buildLowerColumn(LandingViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(viewModel),
);
List<Widget> _buildLowerColumnChildren(LandingViewModel viewModel) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper(viewModel)
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25P600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25P400,
),
TextSpan(
text: ' እድሜ ',
style: style25P600,
),
TextSpan(
text: 'ይማሩ!',
style: style25P400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_2.jpg',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper(LandingViewModel viewModel) =>
SafeArea(child: _buildContinueButtonWrapper(viewModel));
Widget _buildContinueButtonWrapper(LandingViewModel viewModel) => Align(
alignment: Alignment.bottomCenter,
child: _buildButtonContainer(viewModel),
);
Widget _buildButtonContainer(LandingViewModel viewModel) => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButtonState(viewModel),
);
Widget _buildContinueButtonState(LandingViewModel viewModel) =>
viewModel.isBusy ? _buildIndicator() : _buildContinueButton(viewModel);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildContinueButton(LandingViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 25,
text: 'Get Started',
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.setFirstTimeInstall(),
);
}

View File

@ -49,7 +49,7 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
); );
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo_purple.svg',
color: kcPrimaryColor, color: kcPrimaryColor,
height: 25, height: 25,
); );
@ -101,7 +101,7 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
); );
Widget _buildImage() => Image.asset( Widget _buildImage() => Image.asset(
'assets/images/landing_2.jpg', 'assets/images/landing_2.png',
fit: BoxFit.cover, fit: BoxFit.cover,
); );
@ -127,10 +127,10 @@ class SecondLandingScreen extends ViewModelWidget<LandingViewModel> {
Widget _buildContinueButton(LandingViewModel viewModel) => Widget _buildContinueButton(LandingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
text: 'Next',
borderRadius: 25, borderRadius: 25,
onTap: viewModel.next, text: 'Get Started',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.setFirstTimeInstall(),
); );
} }

View File

@ -49,7 +49,7 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
); );
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg', 'assets/icons/logo_purple.svg',
color: kcPrimaryColor, color: kcPrimaryColor,
height: 25, height: 25,
); );
@ -101,7 +101,7 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
); );
Widget _buildImage() => Image.asset( Widget _buildImage() => Image.asset(
'assets/images/landing_3.jpg', 'assets/images/landing_3.png',
fit: BoxFit.cover, fit: BoxFit.cover,
); );
@ -127,10 +127,10 @@ class ThirdLandingScreen extends ViewModelWidget<LandingViewModel> {
Widget _buildContinueButton(LandingViewModel viewModel) => Widget _buildContinueButton(LandingViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
text: 'Next',
borderRadius: 25, borderRadius: 25,
onTap: viewModel.next, text: 'Get Started',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => await viewModel.setFirstTimeInstall(),
); );
} }

View File

@ -152,10 +152,8 @@ class LearnCourseView extends StackedView<LearnCourseViewModel> {
context: context, context: context,
viewModel: viewModel, viewModel: viewModel,
course: viewModel.courses[index]), course: viewModel.courses[index]),
onViewTap: () async => onViewTap: () async => await viewModel.navigateToLearnModule(
await viewModel.navigateToLearnModule( first: first && index == 0, course: viewModel.courses[index]),
first: first && index ==0,
course: viewModel.courses[index]),
), ),
separatorBuilder: (context, index) => verticalSpaceSmall, separatorBuilder: (context, index) => verticalSpaceSmall,
); );

View File

@ -32,8 +32,10 @@ class LearnCourseViewModel extends ReactiveViewModel {
Future<void> navigateToLearnSubscription() async => Future<void> navigateToLearnSubscription() async =>
await _navigationService.navigateToLearnSubscriptionView(); await _navigationService.navigateToLearnSubscriptionView();
Future<void> navigateToLearnModule({required bool first,required LearnCourse course}) async => Future<void> navigateToLearnModule(
_navigationService.navigateToLearnModuleView(first: first,course: course); {required bool first, required LearnCourse course}) async =>
_navigationService.navigateToLearnModuleView(
first: first, course: course);
Future<void> navigateToLearnPractice( Future<void> navigateToLearnPractice(
{required int id, required String level}) async => {required int id, required String level}) async =>

View File

@ -33,7 +33,6 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
required LearnLessonDetailViewModel viewModel}) async { required LearnLessonDetailViewModel viewModel}) async {
await viewModel.pause(); await viewModel.pause();
await viewModel.navigateToLearnPractice(lesson.id ?? 0); await viewModel.navigateToLearnPractice(lesson.id ?? 0);
} }
@override @override

View File

@ -203,10 +203,8 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
viewModel: viewModel, viewModel: viewModel,
module: viewModel.modules[index], module: viewModel.modules[index],
), ),
onModuleTap: () async => onModuleTap: () async => await viewModel.navigateToLearnLesson(
await viewModel.navigateToLearnLesson( first: first && index == 0, module: viewModel.modules[index]),
first: first && index == 0,
module: viewModel.modules[index]),
), ),
); );

View File

@ -35,8 +35,10 @@ class LearnModuleViewModel extends ReactiveViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnLesson({required bool first, required LearnModule module}) async => Future<void> navigateToLearnLesson(
await _navigationService.navigateToLearnLessonView(first:first,module: module); {required bool first, required LearnModule module}) async =>
await _navigationService.navigateToLearnLessonView(
first: first, module: module);
Future<void> navigateToLearnPractice( Future<void> navigateToLearnPractice(
{required int id, required String module}) async => {required int id, required String module}) async =>

View File

@ -61,6 +61,8 @@ class LearnProgramView extends StackedView<LearnProgramViewModel> {
Widget _buildAppBar(LearnProgramViewModel viewModel) => ProfileAppBar( Widget _buildAppBar(LearnProgramViewModel viewModel) => ProfileAppBar(
name: viewModel.user?.firstName, name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture, profileImage: viewModel.user?.profilePicture,
unreadCount: viewModel.unreadCount.toString(),
onTap: () async => await viewModel.navigateToNotification(),
); );
Widget _buildProgramsColumnWrapper(LearnProgramViewModel viewModel) => Widget _buildProgramsColumnWrapper(LearnProgramViewModel viewModel) =>

View File

@ -6,6 +6,7 @@ import 'package:yimaru_app/models/learn_program.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../models/user.dart'; import '../../../models/user.dart';
import '../../../services/authentication_service.dart'; import '../../../services/authentication_service.dart';
import '../../../services/in_app_notification_service.dart';
import '../../../services/learn_service.dart'; import '../../../services/learn_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
@ -21,23 +22,34 @@ class LearnProgramViewModel extends ReactiveViewModel {
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
final _inAppNotificationService = locator<InAppNotificationService>();
@override @override
List<ListenableServiceMixin> get listenableServices => List<ListenableServiceMixin> get listenableServices =>
[_learnService, _authenticationService]; [_learnService, _authenticationService,_inAppNotificationService];
// Current user // Current user
User? get _user => _authenticationService.user; User? get _user => _authenticationService.user;
User? get user => _user; User? get user => _user;
// Notification count
int get _unreadCount => _inAppNotificationService.unreadCount;
int get unreadCount => _unreadCount;
// Learn programs // Learn programs
List<LearnProgram> get _learnPrograms => _learnService.programs; List<LearnProgram> get _learnPrograms => _learnService.programs;
List<LearnProgram> get learnPrograms => _learnPrograms; List<LearnProgram> get learnPrograms => _learnPrograms;
// Navigation // Navigation
Future<void> navigateToLearnCourse({required int id,required bool first}) async => Future<void> navigateToNotification() async =>
_navigationService.navigateToLearnCourseView(id: id,first: first); await _navigationService.navigateToNotificationView();
Future<void> navigateToLearnCourse(
{required int id, required bool first}) async =>
_navigationService.navigateToLearnCourseView(id: id, first: first);
// Remote api call // Remote api call

View File

@ -0,0 +1,112 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/widgets/notification_card.dart';
import '../../../models/in_app_notification.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/translations/locale_keys.g.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import '../../widgets/small_app_bar.dart';
import 'notification_viewmodel.dart';
class NotificationView extends StackedView<NotificationViewModel> {
const NotificationView({Key? key}) : super(key: key);
@override
void onViewModelReady(NotificationViewModel viewModel) async {
await viewModel.getAllNotifications();
await viewModel.markNotificationsRead();
super.onViewModelReady(viewModel);
}
@override
NotificationViewModel viewModelBuilder(BuildContext context) =>
NotificationViewModel();
@override
Widget builder(
BuildContext context,
NotificationViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(NotificationViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldContainer(viewModel),
);
Widget _buildScaffoldContainer(NotificationViewModel viewModel) => Container(
decoration: bgDecoration,
child: _buildScaffold(viewModel),
);
Widget _buildScaffold(NotificationViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(NotificationViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(NotificationViewModel viewModel) => _buildColumn(viewModel);
Widget _buildColumn(NotificationViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel),
);
List<Widget> _buildColumnChildren(NotificationViewModel viewModel) => [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
verticalSpaceMedium,
_buildNotificationsColumnWrapper(viewModel)
];
Widget _buildAppBarWrapper(NotificationViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppbar(viewModel),
);
Widget _buildAppbar(NotificationViewModel viewModel) => SmallAppBar(
showBackButton: true,
onPop: viewModel.pop,
title: LocaleKeys.notifications.tr(),
);
Widget _buildNotificationsColumnWrapper(NotificationViewModel viewModel) =>
Expanded(child: _buildNotificationsColumnScrollView(viewModel));
Widget _buildNotificationsColumnScrollView(NotificationViewModel viewModel) =>
SingleChildScrollView(
child: _buildListViewBuilder(viewModel),
);
Widget _buildListViewBuilder(NotificationViewModel viewModel) =>
viewModel.busy(StateObjects.notifications)
? _buildProgressIndicator()
: _buildListView(viewModel);
Widget _buildProgressIndicator() => const Center(
child: CustomCircularProgressIndicator(color: kcPrimaryColor),
);
Widget _buildListView(NotificationViewModel viewModel) => ListView.separated(
shrinkWrap: true,
itemCount: viewModel.notifications.length,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (context, index) => verticalSpaceSmall,
itemBuilder: (context, index) => _buildCard(
notification: viewModel.notifications[index],
),
);
Widget _buildCard({
required InAppNotification notification,
}) =>
NotificationCard(notification: notification);
}

View File

@ -0,0 +1,72 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
import '../../../models/in_app_notification.dart';
import '../../../services/in_app_notification_service.dart';
import '../../../services/localization_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
class NotificationViewModel extends ReactiveViewModel {
// Dependency injection
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
final _localizationService = locator<LocalizationService>();
final _inAppNotificationService = locator<InAppNotificationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_localizationService, _inAppNotificationService];
// Languages
Map<String, dynamic> get _selectedLanguage =>
_localizationService.selectedLanguage;
Map<String, dynamic> get selectedLanguage => _selectedLanguage;
// Notifications
List<InAppNotification> get _notifications =>
_inAppNotificationService.notifications;
List<InAppNotification> get notifications => _notifications;
// Notification count
int get _unreadCount => _inAppNotificationService.unreadCount;
int get unreadCount => _unreadCount;
// Navigation
void pop() => _navigationService.back();
// Remote api call
// Notifications
Future<void> getAllNotifications() async =>
await runBusyFuture(_getAllNotifications(),
busyObject: StateObjects.notifications);
Future<void> _getAllNotifications() async {
if (await _statusChecker.checkConnection()) {
await _inAppNotificationService.getAllNotifications();
}
}
Future<void> getUnreadNotifications() async =>
await runBusyFuture(_getUnreadNotifications());
Future<void> _getUnreadNotifications() async {
if (await _statusChecker.checkConnection()) {
await _inAppNotificationService.getUnreadNotifications();
}
}
Future<void> markNotificationsRead() async {
if (await _statusChecker.checkConnection()) {
await _inAppNotificationService.markNotificationRead();
}
}
}

View File

@ -5,6 +5,7 @@ import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart'; import 'package:yimaru_app/ui/common/translations/locale_keys.g.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/notification_icon.dart';
import 'package:yimaru_app/ui/widgets/profile_card.dart'; import 'package:yimaru_app/ui/widgets/profile_card.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart'; import 'package:yimaru_app/ui/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/view_profile_button.dart'; import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
@ -89,8 +90,9 @@ class ProfileView extends StackedView<ProfileViewModel> {
required ProfileViewModel viewModel}) => required ProfileViewModel viewModel}) =>
Column( Column(
children: [ children: [
verticalSpaceSmall,
verticalSpaceMedium, verticalSpaceMedium,
_buildNotificationIconWrapper(), _buildNotificationIconWrapper(viewModel),
_buildProfileSection(context: context, viewModel: viewModel), _buildProfileSection(context: context, viewModel: viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildViewProfileButton(viewModel), _buildViewProfileButton(viewModel),
@ -102,12 +104,10 @@ class ProfileView extends StackedView<ProfileViewModel> {
], ],
); );
Widget _buildNotificationIconWrapper() => Widget _buildNotificationIconWrapper(ProfileViewModel viewModel) =>
Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon()); NotificationIcon(
count: viewModel.unreadCount.toString(),
Widget _buildNotificationIcon() => const Icon( onTap: () async => await viewModel.navigateToNotification(),
Icons.notifications_none,
color: kcDarkGrey,
); );
Widget _buildProfileSection( Widget _buildProfileSection(

View File

@ -11,6 +11,7 @@ import '../../../models/user.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart'; import '../../../services/authentication_service.dart';
import '../../../services/google_auth_service.dart'; import '../../../services/google_auth_service.dart';
import '../../../services/in_app_notification_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
@ -23,21 +24,26 @@ class ProfileViewModel extends ReactiveViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _googleAuthService = locator<GoogleAuthService>();
final _imagePickerService = locator<ImagePickerService>(); final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
final _inAppNotificationService = locator<InAppNotificationService>();
@override @override
List<ListenableServiceMixin> get listenableServices => List<ListenableServiceMixin> get listenableServices =>
[_authenticationService]; [_authenticationService,_inAppNotificationService];
// Current user // Current user
User? get _user => _authenticationService.user; User? get _user => _authenticationService.user;
User? get user => _user; User? get user => _user;
// Notification count
int get _unreadCount => _inAppNotificationService.unreadCount;
int get unreadCount => _unreadCount;
// Image picker // Image picker
Future<void> openCamera() async => Future<void> openCamera() async =>
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage); runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
@ -80,21 +86,24 @@ class ProfileViewModel extends ReactiveViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToProfileDetail() async => Future<void> navigateToSupport() async =>
await _navigationService.navigateToProfileDetailView(); await _navigationService.navigateToSupportView();
Future<void> navigateToDownloads() async =>
await _navigationService.navigateToDownloadsView();
Future<void> navigateToProgress() async => Future<void> navigateToProgress() async =>
await _navigationService.navigateToProgressView(); await _navigationService.navigateToProgressView();
Future<void> navigateToDownloads() async =>
await _navigationService.navigateToDownloadsView();
Future<void> navigateToNotification() async =>
await _navigationService.navigateToNotificationView();
Future<void> navigateToProfileDetail() async =>
await _navigationService.navigateToProfileDetailView();
Future<void> navigateToAccountPrivacy() async => Future<void> navigateToAccountPrivacy() async =>
await _navigationService.navigateToAccountPrivacyView(); await _navigationService.navigateToAccountPrivacyView();
Future<void> navigateToSupport() async =>
await _navigationService.navigateToSupportView();
Future<void> navigateToLogin() async => Future<void> navigateToLogin() async =>
await _navigationService.clearStackAndShow(Routes.loginView); await _navigationService.clearStackAndShow(Routes.loginView);

View File

@ -0,0 +1,151 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/startup/startup_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../common/translations/locale_keys.g.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
class FirstStartupScreen extends ViewModelWidget<StartupViewModel> {
final String? label;
const FirstStartupScreen({super.key,this.label});
@override
Widget build(BuildContext context, StartupViewModel viewModel) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper( ) => Scaffold(
backgroundColor: kcPrimaryColor,
body: _buildScaffoldPadding(),
);
Widget _buildScaffoldPadding( ) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(),
);
Widget _buildScaffold( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren( ) =>
[_buildUpperColumn(), _buildLowerColumnWrapper()];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo_white.svg',
height: 25,
);
Widget _buildLowerColumnWrapper( ) => Expanded(
child: _buildLowerColumn(),
);
Widget _buildLowerColumn( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(),
);
List<Widget> _buildLowerColumnChildren( ) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper()
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25W600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25W400,
),
TextSpan(
text: ' ሰዓት ',
style: style25W600,
),
TextSpan(
text: 'ይማሩ!',
style: style25W400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_1.png',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper( ) =>
SafeArea(child: _buildContinueButtonWrapper());
Widget _buildContinueButtonWrapper( ) => Align(
alignment: Alignment.bottomCenter,
child: _buildLoadingTextContainer(),
);
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
];
Widget _buildLoadingText() =>
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16W600);
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
}

View File

@ -0,0 +1,153 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../common/translations/locale_keys.g.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
import '../startup_viewmodel.dart';
class SecondStartupScreen extends ViewModelWidget<StartupViewModel> {
final String? label;
const SecondStartupScreen({super.key,this.label});
@override
Widget build(BuildContext context, StartupViewModel viewModel) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper( ) => Scaffold(
backgroundColor: Colors.amber,
body: _buildScaffoldPadding(),
);
Widget _buildScaffoldPadding( ) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(),
);
Widget _buildScaffold( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren( ) =>
[_buildUpperColumn(), _buildLowerColumnWrapper()];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo_purple.svg',
height: 25,
);
Widget _buildLowerColumnWrapper( ) => Expanded(
child: _buildLowerColumn(),
);
Widget _buildLowerColumn( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(),
);
List<Widget> _buildLowerColumnChildren( ) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper()
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25P600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25P400,
),
TextSpan(
text: ' እድሜ ',
style: style25P600,
),
TextSpan(
text: 'ይማሩ!',
style: style25P400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_2.png',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper( ) =>
SafeArea(child: _buildContinueButtonWrapper());
Widget _buildContinueButtonWrapper( ) => Align(
alignment: Alignment.bottomCenter,
child: _buildLoadingTextContainer(),
);
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
];
Widget _buildLoadingText() =>
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16P600);
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
}

View File

@ -0,0 +1,153 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import '../../../common/translations/locale_keys.g.dart';
import '../../../widgets/custom_circular_progress_indicator.dart';
import '../startup_viewmodel.dart';
class ThirdStartupScreen extends ViewModelWidget<StartupViewModel> {
final String? label;
const ThirdStartupScreen({super.key,this.label});
@override
Widget build(BuildContext context, StartupViewModel viewModel) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper( ) => Scaffold(
backgroundColor:kcWhite,
body: _buildScaffoldPadding(),
);
Widget _buildScaffoldPadding( ) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildScaffold(),
);
Widget _buildScaffold( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren( ) =>
[_buildUpperColumn(), _buildLowerColumnWrapper()];
Widget _buildUpperColumn() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[verticalSpaceLarge, _buildIconWrapper(), verticalSpaceLarge];
Widget _buildIconWrapper() => Align(
alignment: Alignment.topLeft,
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo_purple.svg',
height: 25,
);
Widget _buildLowerColumnWrapper( ) => Expanded(
child: _buildLowerColumn(),
);
Widget _buildLowerColumn( ) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildLowerColumnChildren(),
);
List<Widget> _buildLowerColumnChildren( ) => [
_buildTitle(),
verticalSpaceMedium,
_buildImageWrapper(),
verticalSpaceMedium,
_buildSafeWrapper()
];
Widget _buildTitle() => Text.rich(
TextSpan(
text: 'እንግሊዝኛ\n',
style: style25P600,
children: [
TextSpan(
text: 'በማንኛውም',
style: style25P400,
),
TextSpan(
text: ' ቦታ ',
style: style25P600,
),
TextSpan(
text: 'ይማሩ!',
style: style25P400,
),
],
),
);
Widget _buildImageWrapper() => Expanded(child: _buildImageClipper());
Widget _buildImageClipper() => ClipRRect(
borderRadius: BorderRadius.circular(25),
child: _buildImage(),
);
Widget _buildImage() => Image.asset(
'assets/images/landing_3.png',
fit: BoxFit.cover,
);
Widget _buildSafeWrapper( ) =>
SafeArea(child: _buildContinueButtonWrapper());
Widget _buildContinueButtonWrapper( ) => Align(
alignment: Alignment.bottomCenter,
child: _buildLoadingTextContainer(),
);
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
];
Widget _buildLoadingText() =>
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16P600);
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcPrimaryColor);
}

View File

@ -1,9 +1,13 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_carousel_widget/flutter_carousel_widget.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/views/startup/screens/first_startup_screen.dart';
import 'package:yimaru_app/ui/views/startup/screens/second_startup_screen.dart';
import 'package:yimaru_app/ui/views/startup/screens/third_startup_screen.dart';
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart'; import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
@ -13,6 +17,7 @@ import 'startup_viewmodel.dart';
class StartupView extends StackedView<StartupViewModel> { class StartupView extends StackedView<StartupViewModel> {
final String? label; final String? label;
const StartupView({Key? key, this.label}) : super(key: key); const StartupView({Key? key, this.label}) : super(key: key);
@override @override
@ -24,83 +29,36 @@ class StartupView extends StackedView<StartupViewModel> {
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(StartupViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(StartupViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcPrimaryColor,
body: _buildScaffoldState(viewModel), body: _buildStartupScreens(viewModel),
); );
Widget _buildScaffoldState(StartupViewModel viewModel) => Widget _buildStartupScreens(StartupViewModel viewModel) => FlutterCarousel(
viewModel.busy(StateObjects.startupView) options: FlutterCarouselOptions(
? _buildStartUpView() autoPlay: true,
: _buildScaffold(); viewportFraction: 1,
showIndicator: false,
Widget _buildStartUpView() => height: double.maxFinite,
StartupView(label: LocaleKeys.checking_user_info.tr()); ),
items: _buildScreens(),
Widget _buildScaffold() => Stack(
children: _buildScaffoldChildren(),
); );
List<Widget> _buildScaffoldChildren() => [ List<Widget> _buildScreens() => [
_buildBackground(), _buildFirstStartup(),
_buildColumn(), _buildSecondStartup(),
_buildThirdWelcome(),
]; ];
Widget _buildBackground() => Image.asset( Widget _buildFirstStartup() => FirstStartupScreen(
'assets/images/loading.png', label: label,
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
); );
Widget _buildColumn() => Column( Widget _buildSecondStartup() => SecondStartupScreen(
mainAxisSize: MainAxisSize.max, label: label,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildUpperColumnChildren(),
); );
List<Widget> _buildUpperColumnChildren() => Widget _buildThirdWelcome() => ThirdStartupScreen(
[_buildIconWrapper(), _buildSafeWrapper()]; label: label,
Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer());
Widget _buildLoadingTextContainer() => Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildLoadingTextWrapper(),
);
Widget _buildLoadingTextWrapper() => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: _buildLoadingTextChildren(),
);
List<Widget> _buildLoadingTextChildren() => [
_buildLoadingText(),
horizontalSpaceSmall,
_buildIndicatorWrapper(),
];
Widget _buildLoadingText() =>
Text('${label ?? LocaleKeys.loading.tr()} ...', style: style16W600);
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildIconWrapper() => Padding(
padding: const EdgeInsets.only(top: 120),
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
height: 50,
); );
@override @override

View File

@ -8,6 +8,7 @@ import '../../../app/app.router.dart';
import '../../../models/user.dart'; import '../../../models/user.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../../services/image_downloader_service.dart'; import '../../../services/image_downloader_service.dart';
import '../../../services/in_app_notification_service.dart';
import '../../../services/localization_service.dart'; import '../../../services/localization_service.dart';
import '../../../services/status_checker_service.dart'; import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
@ -28,6 +29,7 @@ class StartupViewModel extends ReactiveViewModel {
final _imageDownloaderService = locator<ImageDownloaderService>(); final _imageDownloaderService = locator<ImageDownloaderService>();
@override @override
List<ListenableServiceMixin> get listenableServices => List<ListenableServiceMixin> get listenableServices =>
[_onboardingService, _authenticationService]; [_onboardingService, _authenticationService];
@ -133,8 +135,6 @@ class StartupViewModel extends ReactiveViewModel {
} }
} }
// Remote api call
// Onboarding fields // Onboarding fields
Future<void> getOnboardingFields() async { Future<void> getOnboardingFields() async {
bool response = await _onboardingService.getOnboardingFields(); bool response = await _onboardingService.getOnboardingFields();
@ -144,4 +144,6 @@ class StartupViewModel extends ReactiveViewModel {
await replaceWithFailure(); await replaceWithFailure();
} }
} }
} }

View File

@ -37,9 +37,13 @@ class LearnLessonTile extends ViewModelWidget<LearnLessonViewModel> {
Widget _buildContainerWrapper(LearnLessonViewModel viewModel) => Widget _buildContainerWrapper(LearnLessonViewModel viewModel) =>
GestureDetector( GestureDetector(
onTap: viewModel.user?.subscriptionStatus?.toLowerCase() == 'active' ? !(lesson.access?.isAccessible ?? false) onTap: viewModel.user?.subscriptionStatus?.toLowerCase() == 'active'
? onPracticeTap ? !(lesson.access?.isAccessible ?? false)
: null: !first ? onPracticeTap:null, ? onPracticeTap
: null
: !first
? onPracticeTap
: null,
child: _buildContainer(viewModel), child: _buildContainer(viewModel),
); );

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/models/in_app_notification.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
class NotificationCard extends StatelessWidget {
final InAppNotification notification;
const NotificationCard({super.key, required this.notification});
@override
Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container(
height: 100,
width: double.maxFinite,
margin: const EdgeInsets.symmetric(horizontal: 15),
padding: const EdgeInsets.symmetric(horizontal: 15,vertical: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: (notification.isRead ?? false)
? kcGreen.withOpacity(0.05)
: kcPrimaryColor.withOpacity(0.05),
border: Border.all(
color: (notification.isRead ?? false)
? kcGreen.withOpacity(0.1)
: kcPrimaryColor.withOpacity(0.1),
),
),
child: _buildRow(),
);
Widget _buildRow() => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildRowChildren(),
);
List<Widget> _buildRowChildren()=> [
_buildIcon(),
horizontalSpaceSmall,
_buildColumnWrapper()
];
Widget _buildIcon() => const Icon(
Icons.notifications_none,
size: 35,
color: kcMediumGrey,
);
Widget _buildColumnWrapper()=> Expanded(child: _buildColumn());
Widget _buildColumn() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(),
);
List<Widget> _buildColumnChildren() =>
[ _buildTitle(), _buildSubtitle()];
Widget _buildTitle() => Text(
notification.payload?.headline ?? '',
style: style16DG600,
);
Widget _buildSubtitle() => Text(
notification.payload?.message ?? '',
maxLines: 2,
style: style14MG400,
);
}

View File

@ -0,0 +1,34 @@
import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';
import 'package:badges/badges.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../common/app_colors.dart';
class NotificationIcon extends StatelessWidget {
final String count;
final GestureTapCallback? onTap;
const NotificationIcon({super.key,this.onTap,required this.count});
@override
Widget build(BuildContext context) => _buildNotificationIconWrapper();
Widget _buildNotificationIconWrapper() => Align(
alignment: Alignment.bottomRight,
child: _buildNotificationButton());
Widget _buildNotificationButton() =>
GestureDetector(
onTap: onTap,
child: _buildNotificationBadge(),
);
Widget _buildNotificationBadge()=> badges.Badge(
badgeContent: Text(count,style: style12W600,),
child: _buildNotificationIcon(),
);
Widget _buildNotificationIcon() => const Icon(
Icons.notifications_none,
color: kcDarkGrey,
);
}

View File

@ -7,13 +7,16 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../common/app_colors.dart'; import '../common/app_colors.dart';
import '../common/translations/locale_keys.g.dart'; import '../common/translations/locale_keys.g.dart';
import 'notification_icon.dart';
class ProfileAppBar extends StatelessWidget { class ProfileAppBar extends StatelessWidget {
final String? name; final String? name;
final String unreadCount;
final String? profileImage; final String? profileImage;
final GestureTapCallback? onTap;
const ProfileAppBar( const ProfileAppBar(
{super.key, required this.name, required this.profileImage}); {super.key, this.onTap, required this.name,required this.unreadCount, required this.profileImage});
@override @override
Widget build(BuildContext context) => _buildStack(); Widget build(BuildContext context) => _buildStack();
@ -91,11 +94,8 @@ class ProfileAppBar extends StatelessWidget {
style: style14DG400, style: style14DG400,
); );
Widget _buildNotificationIconWrapper() =>
Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon());
Widget _buildNotificationIcon() => const Icon( Widget _buildNotificationIconWrapper() =>
Icons.notifications_none, NotificationIcon(count: unreadCount,onTap:onTap ,)
color: kcDarkGrey, ;
);
} }

View File

@ -113,6 +113,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.0" version: "4.3.0"
badges:
dependency: "direct main"
description:
name: badges
sha256: cf1c88fb3777df69ccd630b80de5267f54efa4a39381b0404a7c03d56cb7c041
url: "https://pub.dev"
source: hosted
version: "3.2.0"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,6 +1,6 @@
name: yimaru_app name: yimaru_app
publish_to: 'none' publish_to: 'none'
version: 0.1.35+37 version: 0.1.36+38
description: A new Flutter project. description: A new Flutter project.
environment: environment:
@ -16,6 +16,7 @@ dependencies:
path: ^1.9.1 path: ^1.9.1
async: ^2.13.1 async: ^2.13.1
pinput: ^6.0.1 pinput: ^6.0.1
badges: ^3.2.0
stacked: ^3.4.0 stacked: ^3.4.0
iconsax: ^0.0.8 iconsax: ^0.0.8
chewie: ^1.13.0 chewie: ^1.13.0

View File

@ -11,7 +11,6 @@ import 'package:yimaru_app/services/permission_handler_service.dart';
import 'package:yimaru_app/services/image_picker_service.dart'; import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/services/google_auth_service.dart'; import 'package:yimaru_app/services/google_auth_service.dart';
import 'package:yimaru_app/services/image_downloader_service.dart'; import 'package:yimaru_app/services/image_downloader_service.dart';
import 'package:yimaru_app/services/notification_service.dart';
import 'package:yimaru_app/services/smart_auth_service.dart'; import 'package:yimaru_app/services/smart_auth_service.dart';
import 'package:yimaru_app/services/course_service.dart'; import 'package:yimaru_app/services/course_service.dart';
import 'package:yimaru_app/services/audio_player_service.dart'; import 'package:yimaru_app/services/audio_player_service.dart';
@ -23,6 +22,8 @@ import 'package:yimaru_app/services/phone_caller_service.dart';
import 'package:yimaru_app/services/learn_service.dart'; import 'package:yimaru_app/services/learn_service.dart';
import 'package:yimaru_app/services/localization_service.dart'; import 'package:yimaru_app/services/localization_service.dart';
import 'package:yimaru_app/services/onboarding_service.dart'; import 'package:yimaru_app/services/onboarding_service.dart';
import 'package:yimaru_app/services/in_app_notification_service.dart';
import 'package:yimaru_app/services/push_notification_service.dart';
// @stacked-import // @stacked-import
@GenerateMocks( @GenerateMocks(
@ -57,6 +58,10 @@ import 'package:yimaru_app/services/onboarding_service.dart';
MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<LearnService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<LocalizationService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<LocalizationService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<OnboardingService>(onMissingStub: OnMissingStub.returnDefault), MockSpec<OnboardingService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<InAppNotificationService>(
onMissingStub: OnMissingStub.returnDefault),
MockSpec<PushNotificationService>(
onMissingStub: OnMissingStub.returnDefault),
// @stacked-mock-spec // @stacked-mock-spec
], ],
) )
@ -88,6 +93,8 @@ void registerServices() {
getAndRegisterLearnService(); getAndRegisterLearnService();
getAndRegisterLocalizationService(); getAndRegisterLocalizationService();
getAndRegisterOnboardingService(); getAndRegisterOnboardingService();
getAndRegisterInAppNotificationService();
getAndRegisterPushNotificationService();
// @stacked-mock-register // @stacked-mock-register
} }
@ -208,13 +215,6 @@ MockImageDownloaderService getAndRegisterImageDownloaderService() {
return service; return service;
} }
MockNotificationService getAndRegisterNotificationService() {
_removeRegistrationIfExists<NotificationService>();
final service = MockNotificationService();
locator.registerSingleton<NotificationService>(service);
return service;
}
MockSmartAuthService getAndRegisterSmartAuthService() { MockSmartAuthService getAndRegisterSmartAuthService() {
_removeRegistrationIfExists<SmartAuthService>(); _removeRegistrationIfExists<SmartAuthService>();
final service = MockSmartAuthService(); final service = MockSmartAuthService();
@ -291,6 +291,20 @@ MockOnboardingService getAndRegisterOnboardingService() {
locator.registerSingleton<OnboardingService>(service); locator.registerSingleton<OnboardingService>(service);
return service; return service;
} }
MockInAppNotificationService getAndRegisterInAppNotificationService() {
_removeRegistrationIfExists<InAppNotificationService>();
final service = MockInAppNotificationService();
locator.registerSingleton<InAppNotificationService>(service);
return service;
}
MockPushNotificationService getAndRegisterPushNotificationService() {
_removeRegistrationIfExists<PushNotificationService>();
final service = MockPushNotificationService();
locator.registerSingleton<PushNotificationService>(service);
return service;
}
// @stacked-mock-create // @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() { void _removeRegistrationIfExists<T extends Object>() {

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('InAppNotificationServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart'; import '../helpers/test_helpers.dart';
void main() { void main() {
group('NotificationServiceTest -', () { group('PushNotificationServiceTest -', () {
setUp(() => registerServices()); setUp(() => registerServices());
tearDown(() => locator.reset()); tearDown(() => locator.reset());
}); });

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('NotificationViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}