Merge branch 'release/0.1.0-internal.v1'

- refactor(auth): Apply code refactor for login, register and forget
  password functionalities
This commit is contained in:
BisratHailu 2026-02-12 18:02:27 +03:00
commit 757dfad4e8
171 changed files with 7447 additions and 1248 deletions

View File

@ -1,12 +1,12 @@
plugins { plugins {
id("com.android.application")
id("kotlin-android") id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("com.android.application")
id("com.google.gms.google-services")
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
android { android {
namespace = "com.example.yimaru_app" namespace = "com.yimaru.lms.app"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@ -15,25 +15,24 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlin {
jvmTarget = JavaVersion.VERSION_17.toString() compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.yimaru_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion applicationId = "com.yimaru.lms.app"
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
targetSdk = flutter.targetSdkVersion
} }
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
} }
} }

View File

@ -0,0 +1,51 @@
{
"project_info": {
"project_number": "574860813475",
"project_id": "yimaru-lms-e834e",
"storage_bucket": "yimaru-lms-e834e.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:574860813475:android:cd7fa6cf3a0527d97acb16",
"android_client_info": {
"package_name": "com.yimaru.lms.app"
}
},
"oauth_client": [
{
"client_id": "574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.yimaru.lms.app",
"certificate_hash": "fc91f52846d27c62bba3e16bc98982fb9953eca1"
}
},
{
"client_id": "574860813475-631s3mo8ha2qc2jeb5e2aosn0967niik.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.yimaru.lms.app",
"certificate_hash": "928ead08b5e39d6a861a55ae7cceb8c402d1ee7a"
}
}
],
"api_key": [
{
"current_key": "AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@ -1,6 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<application <application
android:label="yimaru_app" android:label="Yimaru"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@ -1,4 +1,4 @@
package com.example.yimaru_app package com.yimaru.lms.app
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

View File

@ -19,8 +19,10 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false id("com.android.application") version "8.13.2" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false id("org.jetbrains.kotlin.android") version "2.3.0" apply false
id("com.google.gms.google-services") version("4.4.4") apply false
} }
include(":app") include(":app")

5
assets/icons/a_1.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.9409C116.627 97.7416 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7416 8.37341 93.9409V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94918C59.7122 3.33638 65.2878 3.33638 69.8135 5.94918L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94918Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M40.0531 83.9074L50.2659 55.9819H57.4467L67.5797 83.9074H61.1569L59.0026 77.8037H48.3909L46.2366 83.9074H40.0531ZM49.9866 72.9766H57.367L53.6569 62.3649L49.9866 72.9766ZM79.9963 55.9819V83.9074H74.2516V66.075C73.9059 66.208 73.4006 66.2745 72.7357 66.2745H68.0681V61.5271H72.2569C72.9484 61.5271 73.467 61.2612 73.8128 60.7292C74.1851 60.1707 74.3713 59.4925 74.3713 58.6947V55.9819H79.9963Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

5
assets/icons/a_2.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.9409C116.627 97.7416 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7416 8.37341 93.9409V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94918C59.7122 3.33638 65.2878 3.33638 69.8135 5.94918L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94918Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M40.0531 83.9074L50.2659 55.9819H57.4467L67.5797 83.9074H61.1569L59.0026 77.8037H48.3909L46.2366 83.9074H40.0531ZM49.9866 72.9766H57.367L53.6569 62.3649L49.9866 72.9766ZM79.5575 70.7027C80.9139 69.6122 81.9112 68.6149 82.5495 67.7106C83.1878 66.8064 83.5069 65.8223 83.5069 64.7585C83.5069 63.4021 83.0947 62.3649 82.2702 61.6468C81.4724 60.9021 80.3952 60.5298 79.0389 60.5298C77.7357 60.5298 76.6718 60.9686 75.8474 61.8463C75.0495 62.7239 74.6506 63.9074 74.6506 65.3968V65.8356H68.866V65.0777C68.866 63.3223 69.2782 61.7266 70.1027 60.2904C70.9537 58.8542 72.1506 57.7372 73.6931 56.9394C75.2357 56.1149 77.0176 55.7027 79.0389 55.7027C82.3101 55.7027 84.8367 56.5005 86.6186 58.0963C88.4272 59.692 89.3314 61.833 89.3314 64.5191C89.3314 66.434 88.8527 68.0963 87.8952 69.5058C86.9644 70.8888 85.5149 72.3782 83.5468 73.9739L77.2835 79.0803H89.4511V83.9074H68.9857V79.4792L79.5575 70.7027Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="125px" height="138px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.808" fill="#9e2891" d="M 55.5,-0.5 C 59.8333,-0.5 64.1667,-0.5 68.5,-0.5C 84.7704,8.97332 101.104,18.4733 117.5,28C 120.738,30.9978 123.071,34.4978 124.5,38.5C 124.5,58.5 124.5,78.5 124.5,98.5C 122.507,103.008 119.507,106.842 115.5,110C 99.2743,118.941 83.2743,128.108 67.5,137.5C 64.1667,137.5 60.8333,137.5 57.5,137.5C 41.2609,128.375 24.9276,119.208 8.5,110C 4.49334,106.842 1.49334,103.008 -0.5,98.5C -0.5,78.5 -0.5,58.5 -0.5,38.5C 1.15508,34.654 3.48842,31.154 6.5,28C 22.8963,18.4733 39.2296,8.97332 55.5,-0.5 Z"/></g>
<g><path style="opacity:1" fill="#e9cee5" d="M 42.5,55.5 C 42.5,64.5 42.5,73.5 42.5,82.5C 47.1667,82.5 51.8333,82.5 56.5,82.5C 51.6946,83.4872 46.6946,83.8205 41.5,83.5C 41.1731,73.985 41.5064,64.6517 42.5,55.5 Z"/></g>
<g><path style="opacity:1" fill="#fdfafc" d="M 42.5,55.5 C 47.1785,55.3342 51.8452,55.5008 56.5,56C 61.2911,57.4119 63.1244,60.5786 62,65.5C 61.1667,67 60,68.1667 58.5,69C 63.8662,71.8444 64.8662,75.8444 61.5,81C 59.9751,82.0086 58.3084,82.5086 56.5,82.5C 51.8333,82.5 47.1667,82.5 42.5,82.5C 42.5,73.5 42.5,64.5 42.5,55.5 Z"/></g>
<g><path style="opacity:1" fill="#fbf7fb" d="M 71.5,55.5 C 73.5,55.5 75.5,55.5 77.5,55.5C 77.5,64.8333 77.5,74.1667 77.5,83.5C 75.5,83.5 73.5,83.5 71.5,83.5C 71.5,77.5 71.5,71.5 71.5,65.5C 69.5,65.5 67.5,65.5 65.5,65.5C 65.5,64.1667 65.5,62.8333 65.5,61.5C 70.5,62.5 72.5,60.5 71.5,55.5 Z"/></g>
<g><path style="opacity:1" fill="#a63d9a" d="M 47.5,60.5 C 50.1873,60.3359 52.854,60.5026 55.5,61C 57.1397,63.7758 56.473,65.7758 53.5,67C 51.5273,67.4955 49.5273,67.6621 47.5,67.5C 47.5,65.1667 47.5,62.8333 47.5,60.5 Z"/></g>
<g><path style="opacity:1" fill="#a33596" d="M 47.5,71.5 C 50.1873,71.3359 52.854,71.5026 55.5,72C 58.033,74.084 58.033,76.084 55.5,78C 52.854,78.4974 50.1873,78.6641 47.5,78.5C 47.5,76.1667 47.5,73.8333 47.5,71.5 Z"/></g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

5
assets/icons/b_1.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.941C116.627 97.7417 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7417 8.37341 93.941V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94919C59.7122 3.33638 65.2878 3.33638 69.8135 5.94919L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94919Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M52.8989 55.9819C56.1436 55.9819 58.6569 56.6202 60.4388 57.8968C62.2207 59.1734 63.1116 60.9819 63.1116 63.3223C63.1116 65.0511 62.6329 66.4473 61.6755 67.5112C60.718 68.575 59.3616 69.2665 57.6063 69.5856C59.6808 69.8516 61.2765 70.5165 62.3936 71.5803C63.5372 72.6441 64.109 74.1335 64.109 76.0484C64.109 78.4952 63.1781 80.4234 61.3164 81.833C59.4547 83.216 56.8217 83.9074 53.4175 83.9074H42.2872V55.9819H52.8989ZM48.2712 67.4713H53.0584C54.335 67.4713 55.3324 67.1654 56.0505 66.5537C56.7686 65.942 57.1276 65.1175 57.1276 64.0803C57.1276 63.0431 56.7686 62.2186 56.0505 61.6069C55.3324 60.9952 54.335 60.6894 53.0584 60.6894H48.2712V67.4713ZM48.2712 79.1601H53.4574C54.8936 79.1601 55.9973 78.8542 56.7686 78.2425C57.5664 77.6308 57.9654 76.7798 57.9654 75.6894C57.9654 74.5457 57.5664 73.6681 56.7686 73.0564C55.9707 72.4181 54.867 72.0989 53.4574 72.0989H48.2712V79.1601ZM77.9315 55.9819V83.9074H72.1868V66.075C71.8411 66.208 71.3358 66.2745 70.6709 66.2745H66.0033V61.5271H70.1921C70.8836 61.5271 71.4022 61.2612 71.748 60.7292C72.1203 60.1707 72.3065 59.4925 72.3065 58.6947V55.9819H77.9315Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

5
assets/icons/b_2.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="125" height="138" viewBox="0 0 125 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.1809 9.40399C60.4724 7.50364 64.5276 7.50364 67.8191 9.40399L111.307 34.512C114.599 36.4123 116.627 39.9243 116.627 43.725V93.941C116.627 97.7417 114.599 101.254 111.307 103.154L67.8191 128.262C64.5276 130.162 60.4724 130.162 57.1809 128.262L13.6926 103.154C10.4011 101.254 8.37341 97.7417 8.37341 93.941V43.725C8.37341 39.9243 10.4011 36.4123 13.6926 34.512L57.1809 9.40399Z" fill="#9E2891"/>
<path d="M55.1865 5.94919C59.7122 3.33638 65.2878 3.33638 69.8135 5.94919L113.302 31.0566C117.827 33.6695 120.616 38.4988 120.616 43.7246V93.9414C120.616 99.1672 117.827 103.996 113.302 106.609L69.8135 131.717C65.2878 134.33 59.7122 134.33 55.1865 131.717L11.6982 106.609C7.17255 103.996 4.38394 99.1672 4.38379 93.9414V43.7246C4.38394 38.4988 7.17255 33.6695 11.6982 31.0566L55.1865 5.94919Z" stroke="#9E2891" stroke-opacity="0.2" stroke-width="7.97872"/>
<path d="M52.8989 55.9819C56.1436 55.9819 58.6569 56.6202 60.4388 57.8968C62.2207 59.1734 63.1116 60.9819 63.1116 63.3223C63.1116 65.0511 62.6329 66.4473 61.6755 67.5112C60.718 68.575 59.3616 69.2665 57.6063 69.5856C59.6808 69.8516 61.2765 70.5165 62.3936 71.5803C63.5372 72.6441 64.109 74.1335 64.109 76.0484C64.109 78.4952 63.1781 80.4234 61.3164 81.833C59.4547 83.216 56.8217 83.9074 53.4175 83.9074H42.2872V55.9819H52.8989ZM48.2712 67.4713H53.0584C54.335 67.4713 55.3324 67.1654 56.0505 66.5537C56.7686 65.942 57.1276 65.1175 57.1276 64.0803C57.1276 63.0431 56.7686 62.2186 56.0505 61.6069C55.3324 60.9952 54.335 60.6894 53.0584 60.6894H48.2712V67.4713ZM48.2712 79.1601H53.4574C54.8936 79.1601 55.9973 78.8542 56.7686 78.2425C57.5664 77.6308 57.9654 76.7798 57.9654 75.6894C57.9654 74.5457 57.5664 73.6681 56.7686 73.0564C55.9707 72.4181 54.867 72.0989 53.4574 72.0989H48.2712V79.1601ZM77.4927 70.7027C78.849 69.6122 79.8464 68.6149 80.4847 67.7106C81.123 66.8064 81.4421 65.8223 81.4421 64.7585C81.4421 63.4021 81.0299 62.3649 80.2054 61.6468C79.4076 60.9021 78.3304 60.5298 76.974 60.5298C75.6709 60.5298 74.607 60.9686 73.7826 61.8463C72.9847 62.7239 72.5858 63.9074 72.5858 65.3968V65.8356H66.8012V65.0777C66.8012 63.3223 67.2134 61.7266 68.0379 60.2904C68.8889 58.8542 70.0858 57.7372 71.6283 56.9394C73.1709 56.1149 74.9528 55.7027 76.974 55.7027C80.2453 55.7027 82.7719 56.5005 84.5538 58.0963C86.3623 59.692 87.2666 61.833 87.2666 64.5191C87.2666 66.434 86.7879 68.0963 85.8304 69.5058C84.8996 70.8888 83.4501 72.3782 81.482 73.9739L75.2187 79.0803H87.3863V83.9074H66.9209V79.4792L77.4927 70.7027Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

1
firebase.json Normal file
View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"yimaru-lms-e834e","appId":"1:574860813475:android:cd7fa6cf3a0527d97acb16","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"yimaru-lms-e834e","configurations":{"android":"1:574860813475:android:cd7fa6cf3a0527d97acb16","ios":"1:574860813475:ios:3ac9f7c4ae1771287acb16"}}}}}}

View File

@ -368,7 +368,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -547,7 +547,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +569,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.yimaruApp; PRODUCT_BUNDLE_IDENTIFIER = com.yimaru.lms.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -30,6 +30,14 @@ import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/views/welcome/welcome_view.dart'; import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart'; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'; import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import 'package:yimaru_app/services/permission_handler_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/image_downloader_service.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
// @stacked-import // @stacked-import
@StackedApp( @StackedApp(
@ -57,6 +65,10 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
MaterialRoute(page: WelcomeView), MaterialRoute(page: WelcomeView),
MaterialRoute(page: AssessmentView), MaterialRoute(page: AssessmentView),
MaterialRoute(page: LearnLessonView), MaterialRoute(page: LearnLessonView),
MaterialRoute(page: FailureView),
MaterialRoute(page: ForgetPasswordView),
MaterialRoute(page: LearnLessonDetailView),
MaterialRoute(page: LearnPracticeView),
// @stacked-route // @stacked-route
], ],
dependencies: [ dependencies: [
@ -68,6 +80,10 @@ import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
LazySingleton(classType: SecureStorageService), LazySingleton(classType: SecureStorageService),
LazySingleton(classType: DioService), LazySingleton(classType: DioService),
LazySingleton(classType: StatusCheckerService), LazySingleton(classType: StatusCheckerService),
LazySingleton(classType: PermissionHandlerService),
LazySingleton(classType: ImagePickerService),
LazySingleton(classType: GoogleAuthService),
LazySingleton(classType: ImageDownloaderService),
// @stacked-service // @stacked-service
], ],
bottomsheets: [ bottomsheets: [

View File

@ -14,6 +14,10 @@ import 'package:stacked_shared/stacked_shared.dart';
import '../services/api_service.dart'; import '../services/api_service.dart';
import '../services/authentication_service.dart'; import '../services/authentication_service.dart';
import '../services/dio_service.dart'; import '../services/dio_service.dart';
import '../services/google_auth_service.dart';
import '../services/image_downloader_service.dart';
import '../services/image_picker_service.dart';
import '../services/permission_handler_service.dart';
import '../services/secure_storage_service.dart'; import '../services/secure_storage_service.dart';
import '../services/status_checker_service.dart'; import '../services/status_checker_service.dart';
@ -36,4 +40,8 @@ Future<void> setupLocator({
locator.registerLazySingleton(() => SecureStorageService()); locator.registerLazySingleton(() => SecureStorageService());
locator.registerLazySingleton(() => DioService()); locator.registerLazySingleton(() => DioService());
locator.registerLazySingleton(() => StatusCheckerService()); locator.registerLazySingleton(() => StatusCheckerService());
locator.registerLazySingleton(() => PermissionHandlerService());
locator.registerLazySingleton(() => ImagePickerService());
locator.registerLazySingleton(() => GoogleAuthService());
locator.registerLazySingleton(() => ImageDownloaderService());
} }

View File

@ -5,24 +5,31 @@
// ************************************************************************** // **************************************************************************
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter/material.dart' as _i25; import 'package:flutter/material.dart' as _i29;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart' as _i1; import 'package:stacked/stacked.dart' as _i1;
import 'package:stacked_services/stacked_services.dart' as _i26; import 'package:stacked_services/stacked_services.dart' as _i30;
import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'
as _i10; as _i10;
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23; import 'package:yimaru_app/ui/views/assessment/assessment_view.dart' as _i23;
import 'package:yimaru_app/ui/views/call_support/call_support_view.dart' import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'
as _i13; as _i13;
import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7;
import 'package:yimaru_app/ui/views/failure/failure_view.dart' as _i25;
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.dart'
as _i26;
import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2; import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2;
import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14; import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14;
import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19; import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19;
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart' import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart'
as _i24; as _i24;
import 'package:yimaru_app/ui/views/learn_lesson_detail/learn_lesson_detail_view.dart'
as _i27;
import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20; import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20;
import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart' import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'
as _i21; as _i21;
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart'
as _i28;
import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18; import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18;
import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3;
import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart' import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'
@ -89,6 +96,14 @@ class Routes {
static const learnLessonView = '/learn-lesson-view'; static const learnLessonView = '/learn-lesson-view';
static const failureView = '/failure-view';
static const forgetPasswordView = '/forget-password-view';
static const learnLessonDetailView = '/learn-lesson-detail-view';
static const learnPracticeView = '/learn-practice-view';
static const all = <String>{ static const all = <String>{
homeView, homeView,
onboardingView, onboardingView,
@ -113,6 +128,10 @@ class Routes {
welcomeView, welcomeView,
assessmentView, assessmentView,
learnLessonView, learnLessonView,
failureView,
forgetPasswordView,
learnLessonDetailView,
learnPracticeView,
}; };
} }
@ -210,17 +229,33 @@ class StackedRouter extends _i1.RouterBase {
Routes.learnLessonView, Routes.learnLessonView,
page: _i24.LearnLessonView, page: _i24.LearnLessonView,
), ),
_i1.RouteDef(
Routes.failureView,
page: _i25.FailureView,
),
_i1.RouteDef(
Routes.forgetPasswordView,
page: _i26.ForgetPasswordView,
),
_i1.RouteDef(
Routes.learnLessonDetailView,
page: _i27.LearnLessonDetailView,
),
_i1.RouteDef(
Routes.learnPracticeView,
page: _i28.LearnPracticeView,
),
]; ];
final _pagesMap = <Type, _i1.StackedRouteFactory>{ final _pagesMap = <Type, _i1.StackedRouteFactory>{
_i2.HomeView: (data) { _i2.HomeView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i2.HomeView(), builder: (context) => const _i2.HomeView(),
settings: data, settings: data,
); );
}, },
_i3.OnboardingView: (data) { _i3.OnboardingView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i3.OnboardingView(), builder: (context) => const _i3.OnboardingView(),
settings: data, settings: data,
); );
@ -229,133 +264,159 @@ class StackedRouter extends _i1.RouterBase {
final args = data.getArgs<StartupViewArguments>( final args = data.getArgs<StartupViewArguments>(
orElse: () => const StartupViewArguments(), orElse: () => const StartupViewArguments(),
); );
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => _i4.StartupView(key: args.key, label: args.label), builder: (context) => _i4.StartupView(key: args.key, label: args.label),
settings: data, settings: data,
); );
}, },
_i5.ProfileView: (data) { _i5.ProfileView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i5.ProfileView(), builder: (context) => const _i5.ProfileView(),
settings: data, settings: data,
); );
}, },
_i6.ProfileDetailView: (data) { _i6.ProfileDetailView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i6.ProfileDetailView(), builder: (context) => const _i6.ProfileDetailView(),
settings: data, settings: data,
); );
}, },
_i7.DownloadsView: (data) { _i7.DownloadsView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i7.DownloadsView(), builder: (context) => const _i7.DownloadsView(),
settings: data, settings: data,
); );
}, },
_i8.ProgressView: (data) { _i8.ProgressView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i8.ProgressView(), builder: (context) => const _i8.ProgressView(),
settings: data, settings: data,
); );
}, },
_i9.OngoingProgressView: (data) { _i9.OngoingProgressView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i9.OngoingProgressView(), builder: (context) => const _i9.OngoingProgressView(),
settings: data, settings: data,
); );
}, },
_i10.AccountPrivacyView: (data) { _i10.AccountPrivacyView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i10.AccountPrivacyView(), builder: (context) => const _i10.AccountPrivacyView(),
settings: data, settings: data,
); );
}, },
_i11.SupportView: (data) { _i11.SupportView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i11.SupportView(), builder: (context) => const _i11.SupportView(),
settings: data, settings: data,
); );
}, },
_i12.TelegramSupportView: (data) { _i12.TelegramSupportView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i12.TelegramSupportView(), builder: (context) => const _i12.TelegramSupportView(),
settings: data, settings: data,
); );
}, },
_i13.CallSupportView: (data) { _i13.CallSupportView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i13.CallSupportView(), builder: (context) => const _i13.CallSupportView(),
settings: data, settings: data,
); );
}, },
_i14.LanguageView: (data) { _i14.LanguageView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i14.LanguageView(), builder: (context) => const _i14.LanguageView(),
settings: data, settings: data,
); );
}, },
_i15.PrivacyPolicyView: (data) { _i15.PrivacyPolicyView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i15.PrivacyPolicyView(), builder: (context) => const _i15.PrivacyPolicyView(),
settings: data, settings: data,
); );
}, },
_i16.TermsAndConditionsView: (data) { _i16.TermsAndConditionsView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i16.TermsAndConditionsView(), builder: (context) => const _i16.TermsAndConditionsView(),
settings: data, settings: data,
); );
}, },
_i17.RegisterView: (data) { _i17.RegisterView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i17.RegisterView(), builder: (context) => const _i17.RegisterView(),
settings: data, settings: data,
); );
}, },
_i18.LoginView: (data) { _i18.LoginView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i18.LoginView(), builder: (context) => const _i18.LoginView(),
settings: data, settings: data,
); );
}, },
_i19.LearnView: (data) { _i19.LearnView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i19.LearnView(), builder: (context) => const _i19.LearnView(),
settings: data, settings: data,
); );
}, },
_i20.LearnLevelView: (data) { _i20.LearnLevelView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i20.LearnLevelView(), builder: (context) => const _i20.LearnLevelView(),
settings: data, settings: data,
); );
}, },
_i21.LearnModuleView: (data) { _i21.LearnModuleView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i21.LearnModuleView(), builder: (context) => const _i21.LearnModuleView(),
settings: data, settings: data,
); );
}, },
_i22.WelcomeView: (data) { _i22.WelcomeView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i22.WelcomeView(), builder: (context) => const _i22.WelcomeView(),
settings: data, settings: data,
); );
}, },
_i23.AssessmentView: (data) { _i23.AssessmentView: (data) {
final args = data.getArgs<AssessmentViewArguments>(nullOk: false); final args = data.getArgs<AssessmentViewArguments>(nullOk: false);
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => builder: (context) =>
_i23.AssessmentView(key: args.key, data: args.data), _i23.AssessmentView(key: args.key, data: args.data),
settings: data, settings: data,
); );
}, },
_i24.LearnLessonView: (data) { _i24.LearnLessonView: (data) {
return _i25.MaterialPageRoute<dynamic>( return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i24.LearnLessonView(), builder: (context) => const _i24.LearnLessonView(),
settings: data, settings: data,
); );
}, },
_i25.FailureView: (data) {
final args = data.getArgs<FailureViewArguments>(nullOk: false);
return _i29.MaterialPageRoute<dynamic>(
builder: (context) =>
_i25.FailureView(key: args.key, label: args.label),
settings: data,
);
},
_i26.ForgetPasswordView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i26.ForgetPasswordView(),
settings: data,
);
},
_i27.LearnLessonDetailView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i27.LearnLessonDetailView(),
settings: data,
);
},
_i28.LearnPracticeView: (data) {
return _i29.MaterialPageRoute<dynamic>(
builder: (context) => const _i28.LearnPracticeView(),
settings: data,
);
},
}; };
@override @override
@ -371,7 +432,7 @@ class StartupViewArguments {
this.label = 'Loading', this.label = 'Loading',
}); });
final _i25.Key? key; final _i29.Key? key;
final String label; final String label;
@ -398,7 +459,7 @@ class AssessmentViewArguments {
required this.data, required this.data,
}); });
final _i25.Key? key; final _i29.Key? key;
final Map<String, dynamic> data; final Map<String, dynamic> data;
@ -419,7 +480,34 @@ class AssessmentViewArguments {
} }
} }
extension NavigatorStateExtension on _i26.NavigationService { class FailureViewArguments {
const FailureViewArguments({
this.key,
required this.label,
});
final _i29.Key? key;
final String label;
@override
String toString() {
return '{"key": "$key", "label": "$label"}';
}
@override
bool operator ==(covariant FailureViewArguments other) {
if (identical(this, other)) return true;
return other.key == key && other.label == label;
}
@override
int get hashCode {
return key.hashCode ^ label.hashCode;
}
}
extension NavigatorStateExtension on _i30.NavigationService {
Future<dynamic> navigateToHomeView([ Future<dynamic> navigateToHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -449,7 +537,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
} }
Future<dynamic> navigateToStartupView({ Future<dynamic> navigateToStartupView({
_i25.Key? key, _i29.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -718,7 +806,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
} }
Future<dynamic> navigateToAssessmentView({ Future<dynamic> navigateToAssessmentView({
_i25.Key? key, _i29.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -748,6 +836,65 @@ extension NavigatorStateExtension on _i26.NavigationService {
transition: transition); transition: transition);
} }
Future<dynamic> navigateToFailureView({
_i29.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return navigateTo<dynamic>(Routes.failureView,
arguments: FailureViewArguments(key: key, label: label),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToForgetPasswordView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.forgetPasswordView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnLessonDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnLessonDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> navigateToLearnPracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return navigateTo<dynamic>(Routes.learnPracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithHomeView([ Future<dynamic> replaceWithHomeView([
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -777,7 +924,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
} }
Future<dynamic> replaceWithStartupView({ Future<dynamic> replaceWithStartupView({
_i25.Key? key, _i29.Key? key,
String label = 'Loading', String label = 'Loading',
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1046,7 +1193,7 @@ extension NavigatorStateExtension on _i26.NavigationService {
} }
Future<dynamic> replaceWithAssessmentView({ Future<dynamic> replaceWithAssessmentView({
_i25.Key? key, _i29.Key? key,
required Map<String, dynamic> data, required Map<String, dynamic> data,
int? routerId, int? routerId,
bool preventDuplicates = true, bool preventDuplicates = true,
@ -1075,4 +1222,63 @@ extension NavigatorStateExtension on _i26.NavigationService {
parameters: parameters, parameters: parameters,
transition: transition); transition: transition);
} }
Future<dynamic> replaceWithFailureView({
_i29.Key? key,
required String label,
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
}) async {
return replaceWith<dynamic>(Routes.failureView,
arguments: FailureViewArguments(key: key, label: label),
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithForgetPasswordView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.forgetPasswordView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnLessonDetailView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnLessonDetailView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
Future<dynamic> replaceWithLearnPracticeView([
int? routerId,
bool preventDuplicates = true,
Map<String, String>? parameters,
Widget Function(BuildContext, Animation<double>, Animation<double>, Widget)?
transition,
]) async {
return replaceWith<dynamic>(Routes.learnPracticeView,
id: routerId,
preventDuplicates: preventDuplicates,
parameters: parameters,
transition: transition);
}
} }

70
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,70 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyC7QlhcuSNte49CERnRKPrQbyLbwErIRmk',
appId: '1:574860813475:android:cd7fa6cf3a0527d97acb16',
messagingSenderId: '574860813475',
projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyBBcQ17JB6RBTjD7G7mh6Xf_FMUGxP5cC8',
appId: '1:574860813475:ios:3ac9f7c4ae1771287acb16',
messagingSenderId: '574860813475',
projectId: 'yimaru-lms-e834e',
storageBucket: 'yimaru-lms-e834e.firebasestorage.app',
androidClientId:
'574860813475-01gh5tk0bu5bgj68r02sgh5pk5greoku.apps.googleusercontent.com',
iosBundleId: 'com.yimaru.lms.app',
);
}

View File

@ -1,17 +1,35 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:yimaru_app/models/option.dart'; import 'package:yimaru_app/models/option.dart';
import 'package:yimaru_app/models/question.dart';
part 'assessment.g.dart'; part 'assessment.g.dart';
@JsonSerializable() @JsonSerializable()
class Assessment { class Assessment {
@JsonKey(name: 'Question') final int? id;
final Question? question;
final int? points;
final String? status;
@JsonKey(name: 'question_type')
final String? questionType;
@JsonKey(name: 'question_text')
final String? questionText;
@JsonKey(name: 'difficulty_level')
final String? difficultyLevel;
@JsonKey(name: 'Options')
final List<Option>? options; final List<Option>? options;
const Assessment({this.options, this.question}); const Assessment({
this.id,
this.points,
this.status,
this.options,
this.questionText,
this.questionType,
this.difficultyLevel,
});
factory Assessment.fromJson(Map<String, dynamic> json) => factory Assessment.fromJson(Map<String, dynamic> json) =>
_$AssessmentFromJson(json); _$AssessmentFromJson(json);

View File

@ -7,16 +7,24 @@ part of 'assessment.dart';
// ************************************************************************** // **************************************************************************
Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment( Assessment _$AssessmentFromJson(Map<String, dynamic> json) => Assessment(
options: (json['Options'] as List<dynamic>?) id: (json['id'] as num?)?.toInt(),
points: (json['points'] as num?)?.toInt(),
status: json['status'] as String?,
options: (json['options'] as List<dynamic>?)
?.map((e) => Option.fromJson(e as Map<String, dynamic>)) ?.map((e) => Option.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
question: json['Question'] == null questionText: json['question_text'] as String?,
? null questionType: json['question_type'] as String?,
: Question.fromJson(json['Question'] as Map<String, dynamic>), difficultyLevel: json['difficulty_level'] as String?,
); );
Map<String, dynamic> _$AssessmentToJson(Assessment instance) => Map<String, dynamic> _$AssessmentToJson(Assessment instance) =>
<String, dynamic>{ <String, dynamic>{
'Question': instance.question, 'id': instance.id,
'Options': instance.options, 'points': instance.points,
'status': instance.status,
'question_type': instance.questionType,
'question_text': instance.questionText,
'difficulty_level': instance.difficultyLevel,
'options': instance.options,
}; };

View File

@ -3,13 +3,15 @@ part 'option.g.dart';
@JsonSerializable() @JsonSerializable()
class Option { class Option {
@JsonKey(name: 'question_id') final int? id;
final int? questionId;
@JsonKey(name: 'option_text') @JsonKey(name: 'option_text')
final String? optionText; final String? optionText;
const Option({this.optionText, this.questionId}); @JsonKey(name: 'is_correct')
final bool? isCorrect;
const Option({this.id, this.optionText, this.isCorrect});
factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json); factory Option.fromJson(Map<String, dynamic> json) => _$OptionFromJson(json);

View File

@ -7,11 +7,13 @@ part of 'option.dart';
// ************************************************************************** // **************************************************************************
Option _$OptionFromJson(Map<String, dynamic> json) => Option( Option _$OptionFromJson(Map<String, dynamic> json) => Option(
id: (json['id'] as num?)?.toInt(),
optionText: json['option_text'] as String?, optionText: json['option_text'] as String?,
questionId: (json['question_id'] as num?)?.toInt(), isCorrect: json['is_correct'] as bool?,
); );
Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{ Map<String, dynamic> _$OptionToJson(Option instance) => <String, dynamic>{
'question_id': instance.questionId, 'id': instance.id,
'option_text': instance.optionText, 'option_text': instance.optionText,
'is_correct': instance.isCorrect,
}; };

View File

@ -1,36 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
part 'question.g.dart';
@JsonSerializable()
class Question {
final int? id;
final int? points;
final String? title;
final String? description;
@JsonKey(name: 'is_active')
final bool? isActive;
@JsonKey(name: 'question_type')
final String? questionType;
@JsonKey(name: 'difficulty_level')
final String? difficultyLevel;
const Question(
{this.id,
this.title,
this.points,
this.isActive,
this.description,
this.questionType,
this.difficultyLevel});
factory Question.fromJson(Map<String, dynamic> json) =>
_$QuestionFromJson(json);
Map<String, dynamic> toJson() => _$QuestionToJson(this);
}

View File

@ -1,27 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'question.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Question _$QuestionFromJson(Map<String, dynamic> json) => Question(
id: (json['id'] as num?)?.toInt(),
title: json['title'] as String?,
points: (json['points'] as num?)?.toInt(),
isActive: json['is_active'] as bool?,
description: json['description'] as String?,
questionType: json['question_type'] as String?,
difficultyLevel: json['difficulty_level'] as String?,
);
Map<String, dynamic> _$QuestionToJson(Question instance) => <String, dynamic>{
'id': instance.id,
'points': instance.points,
'title': instance.title,
'description': instance.description,
'is_active': instance.isActive,
'question_type': instance.questionType,
'difficulty_level': instance.difficultyLevel,
};

View File

@ -4,10 +4,31 @@ part 'user_model.g.dart';
@JsonSerializable() @JsonSerializable()
class UserModel { class UserModel {
final String? email;
final String? gender;
final String? region;
final String? country;
final String? occupation;
final bool? userInfoLoaded;
@JsonKey(name: 'user_id') @JsonKey(name: 'user_id')
final int? userId; final int? userId;
final bool? profileCompleted; @JsonKey(name: 'last_name')
final String? lastName;
@JsonKey(name: 'birth_day')
final String? birthday;
@JsonKey(name: 'first_name')
final String? firstName;
@JsonKey(name: 'access_token') @JsonKey(name: 'access_token')
final String? accessToken; final String? accessToken;
@ -15,11 +36,28 @@ class UserModel {
@JsonKey(name: 'refresh_token') @JsonKey(name: 'refresh_token')
final String? refreshToken; final String? refreshToken;
const UserModel( @JsonKey(name: 'profile_completed')
{this.userId, final bool? profileCompleted;
this.accessToken,
this.profileCompleted, @JsonKey(name: 'profile_picture_url')
this.refreshToken}); final String? profilePicture;
const UserModel({
this.email,
this.region,
this.gender,
this.userId,
this.country,
this.lastName,
this.birthday,
this.firstName,
this.occupation,
this.accessToken,
this.refreshToken,
this.profilePicture,
this.userInfoLoaded ,
this.profileCompleted,
});
factory UserModel.fromJson(Map<String, dynamic> json) => factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json); _$UserModelFromJson(json);

View File

@ -7,15 +7,35 @@ part of 'user_model.dart';
// ************************************************************************** // **************************************************************************
UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel( UserModel _$UserModelFromJson(Map<String, dynamic> json) => UserModel(
email: json['email'] as String?,
region: json['region'] as String?,
gender: json['gender'] as String?,
userId: (json['user_id'] as num?)?.toInt(), userId: (json['user_id'] as num?)?.toInt(),
country: json['country'] as String?,
lastName: json['last_name'] as String?,
birthday: json['birth_day'] as String?,
firstName: json['first_name'] as String?,
occupation: json['occupation'] as String?,
accessToken: json['access_token'] as String?, accessToken: json['access_token'] as String?,
profileCompleted: json['profileCompleted'] as bool?,
refreshToken: json['refresh_token'] as String?, refreshToken: json['refresh_token'] as String?,
profilePicture: json['profile_picture_url'] as String?,
profileCompleted: json['profile_completed'] as bool?,
userInfoLoaded: json['userInfoLoaded'] as bool? ?? false,
); );
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{ Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
'email': instance.email,
'gender': instance.gender,
'region': instance.region,
'country': instance.country,
'occupation': instance.occupation,
'userInfoLoaded': instance.userInfoLoaded,
'user_id': instance.userId, 'user_id': instance.userId,
'profileCompleted': instance.profileCompleted, 'last_name': instance.lastName,
'birth_day': instance.birthday,
'first_name': instance.firstName,
'access_token': instance.accessToken, 'access_token': instance.accessToken,
'refresh_token': instance.refreshToken, 'refresh_token': instance.refreshToken,
'profile_completed': instance.profileCompleted,
'profile_picture_url': instance.profilePicture,
}; };

View File

@ -11,10 +11,10 @@ class ApiService {
final _service = locator<DioService>(); final _service = locator<DioService>();
// Register // Register
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async { Future<Map<String, dynamic>> registerWithEmail(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$baseUrl/$userUrl/$kRegisterUrl', '$kBaseUrl/$kUserUrl/$kRegisterUrl',
data: data, data: data,
); );
@ -29,19 +29,19 @@ class ApiService {
'message': 'Unknown Error Occurred' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
// Login // Email Login
Future<Map<String, dynamic>> login(Map<String, dynamic> data) async { Future<Map<String, dynamic>> emailLogin(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$baseUrl/$kLoginUrl', '$kBaseUrl/$kLoginUrl',
data: data, data: data,
); );
@ -57,10 +57,38 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}' 'message': '${response.data['message']}, ${response.data['error']}'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Google login
Future<Map<String, dynamic>> googleAuth(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kGoogleAuthUrl',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Logged in successfully',
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -69,14 +97,14 @@ class ApiService {
Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async { Future<Map<String, dynamic>> verifyOtp(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$baseUrl/$userUrl/$kVerifyOtpUrl', '$kBaseUrl/$kUserUrl/$kVerifyOtpUrl',
data: data, data: data,
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
return { return {
'status': ResponseStatus.success, 'status': ResponseStatus.success,
'message': 'Otp verified successfully', 'message': 'Otp verified successfully',
//'data': UserModel.fromJson(response.data['data']), 'data': UserModel.fromJson(response.data['data']),
}; };
} else { } else {
return { return {
@ -84,10 +112,10 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}' 'message': '${response.data['message']}, ${response.data['error']}'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -96,7 +124,7 @@ class ApiService {
Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async { Future<Map<String, dynamic>> resendOtp(Map<String, dynamic> data) async {
try { try {
Response response = await _service.dio.post( Response response = await _service.dio.post(
'$baseUrl/$userUrl/$kResendOtpUrl', '$kBaseUrl/$kUserUrl/$kResendOtpUrl',
data: data, data: data,
); );
@ -111,10 +139,65 @@ class ApiService {
'message': 'Unknown Error Occurred' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Request reset code
Future<Map<String, dynamic>> requestResetCode(
Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kRequestResetCode',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Reset code sent successfully',
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Reset password
Future<Map<String, dynamic>> resetPassword(Map<String, dynamic> data) async {
try {
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$kResetPassword',
data: data,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Password reset successfully',
};
} else {
return {
'status': ResponseStatus.failure,
'message': '${response.data['message']}, ${response.data['error']}'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -123,7 +206,7 @@ class ApiService {
Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async { Future<Map<String, dynamic>> getProfileStatus(UserModel? user) async {
try { try {
Response response = await _service.dio.get( Response response = await _service.dio.get(
'$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl', '$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -138,25 +221,49 @@ class ApiService {
'message': '${response.data['message']}, ${response.data['error']}' 'message': '${response.data['message']}, ${response.data['error']}'
}; };
} }
} catch (e) { } on DioException catch (e) {
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
// Update profile // Get profile
Future<Map<String, dynamic>> updateProfile( Future<Map<String, dynamic>> getProfileData(int? userId) async {
{required UserModel? user, required Map<String, dynamic> data}) async {
try { try {
Response response = await _service.dio.put( Response response = await _service.dio.get(
'$baseUrl/$userUrl', '$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId',
data: data,
); );
print(response.statusCode); if (response.statusCode == 200) {
print(response.data); return {
'status': ResponseStatus.success,
'message': 'Profile fetched successfully',
'data': UserModel.fromJson(response.data['data']),
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Complete profile
Future<Map<String, dynamic>> completeProfile(
Map<String, dynamic> data) async {
try {
Response response = await _service.dio.put(
'$kBaseUrl/$kUserUrl',
data: data,
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
return { return {
@ -169,11 +276,62 @@ class ApiService {
'message': 'Unknown Error Occurred' 'message': 'Unknown Error Occurred'
}; };
} }
} catch (e) { } on DioException catch (e) {
print('Exception ${e.toString()}');
return { return {
'message': e.toString(),
'status': ResponseStatus.failure, 'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
};
}
}
// Update profile image
Future<Map<String, dynamic>> updateProfileImage(
{required int? userId, required Map<String, dynamic> data}) async {
try {
late FormData formData;
if (data['profile_picture_url']
.toString()
.contains('com.example.yimaru_app/')) {
formData = FormData.fromMap({
'file': data['profile_picture_url'].toString().isNotEmpty
? MultipartFile.fromFileSync(
data['profile_picture_url'],
filename:
data['profile_picture_url'].toString().split('/').last,
)
: null,
});
} else {
formData = FormData.fromMap({
'file': data['profile_picture_url'].toString().isNotEmpty
? MultipartFile.fromFileSync(
data['profile_picture_url'],
filename:
data['profile_picture_url'].toString().split('/').last,
)
: null,
});
}
Response response = await _service.dio.post(
'$kBaseUrl/$kUserUrl/$userId/$kUpdateProfileImage',
data: formData,
);
if (response.statusCode == 200) {
return {
'status': ResponseStatus.success,
'message': 'Profile updated successfully'
};
} else {
return {
'status': ResponseStatus.failure,
'message': 'Unknown Error Occurred'
};
}
} on DioException catch (e) {
return {
'status': ResponseStatus.failure,
'message': e.response?.data.toString(),
}; };
} }
} }
@ -184,7 +342,7 @@ class ApiService {
List<Assessment> assessments = []; List<Assessment> assessments = [];
final Response response = final Response response =
await _service.dio.get('$baseUrl/$kAssessmentsUrl'); await _service.dio.get('$kBaseUrl/$kAssessmentsUrl');
if (response.statusCode == 200) { if (response.statusCode == 200) {
var data = response.data; var data = response.data;

View File

@ -1,10 +1,19 @@
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/user_model.dart'; import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/secure_storage_service.dart'; import 'package:yimaru_app/services/secure_storage_service.dart';
class AuthenticationService { class AuthenticationService with ListenableServiceMixin {
final _secureService = locator<SecureStorageService>(); final _secureService = locator<SecureStorageService>();
AuthenticationService() {
listenToReactiveValues([_user]);
}
UserModel? _user;
UserModel? get user => _user;
Future<bool> userLoggedIn() async { Future<bool> userLoggedIn() async {
if (await _secureService.getString('userId') != null) { if (await _secureService.getString('userId') != null) {
return true; return true;
@ -28,14 +37,122 @@ class AuthenticationService {
await _secureService.setString('refreshToken', refresh); await _secureService.setString('refreshToken', refresh);
} }
Future<void> saveUserData(Map<String, dynamic> data) async {
Future<void> saveUserCredential(Map<String, dynamic> data) async {
await _secureService.setInt('userId', data['userId']); await _secureService.setInt('userId', data['userId']);
await _secureService.setString('accessToken', data['accessToken']); await _secureService.setString('accessToken', data['accessToken']);
await _secureService.setString('refreshToken', data['refreshToken']); await _secureService.setString('refreshToken', data['refreshToken']);
_user = UserModel(
userId: await _secureService.getInt('userId'),
accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'),
);
} }
Future<void> saveProfileCompleted(bool value) async { Future<void> saveProfileStatus(bool value) async {
await _secureService.setBool('profileCompleted', value); await _secureService.setBool('profileCompleted', value);
_user = UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
userId: _user?.userId,
country: _user?.country,
lastName: _user?.lastName,
birthday: _user?.birthday,
firstName: _user?.firstName,
occupation: _user?.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profileCompleted: await _secureService.getBool('profileCompleted'));
notifyListeners();
}
Future<void> saveProfileImage(String image) async {
await _secureService.setString('profileImage', image);
_user = UserModel(
email: _user?.email,
gender: _user?.gender,
region: _user?.region,
userId: _user?.userId,
country: _user?.country,
lastName: _user?.lastName,
birthday: _user?.birthday,
firstName: _user?.firstName,
occupation: _user?.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted,
userInfoLoaded: _user?.userInfoLoaded ?? false,
profilePicture: await _secureService.getString('profileImage'),
);
notifyListeners();
}
Future<void> saveUserData(
{required String image, required UserModel data}) async {
await _secureService.setBool('userInfoLoaded', true);
await _secureService.setBool(
'profileCompleted', data.profileCompleted ?? false);
await _secureService.setString('profilePicture', image);
await _secureService.setString('email', data.email ?? '');
await _secureService.setString('region', data.region ?? '');
await _secureService.setString('gender', data.gender ?? '');
await _secureService.setString('country', data.country ?? '');
await _secureService.setString('birthday', data.birthday ?? '');
await _secureService.setString('lastName', data.lastName ?? '');
await _secureService.setString('firstName', data.firstName ?? '');
await _secureService.setString('occupation', data.occupation ?? '');
_user = UserModel(
email: data.email,
gender: data.gender,
region: data.region,
userInfoLoaded: true,
profilePicture: image,
userId: _user?.userId,
country: data.country,
lastName: data.lastName,
birthday: data.birthday,
firstName: data.firstName,
occupation: data.occupation,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: data.profileCompleted,
);
notifyListeners();
}
Future<void> updateUserData(Map<String, dynamic> data) async {
await _secureService.setString('region', data['region']);
await _secureService.setString('gender', data['gender']);
await _secureService.setString('country', data['country']);
await _secureService.setString('lastName', data['last_name']);
await _secureService.setString('birthday', data['birth_day']);
await _secureService.setString('firstName', data['first_name']);
await _secureService.setString('occupation', data['occupation']);
_user = UserModel(
email: _user?.email,
userId: _user?.userId,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profilePicture: _user?.profilePicture,
profileCompleted: _user?.profileCompleted,
region: await _secureService.getString('region'),
gender: await _secureService.getString('gender'),
country: await _secureService.getString('country'),
lastName: await _secureService.getString('lastName'),
birthday: await _secureService.getString('birthday'),
firstName: await _secureService.getString('firstName'),
occupation: await _secureService.getString('occupation'),
);
notifyListeners();
} }
Future<bool> isFirstTimeInstall() async => Future<bool> isFirstTimeInstall() async =>
@ -45,18 +162,29 @@ class AuthenticationService {
await _secureService.setBool('firstTimeInstall', value); await _secureService.setBool('firstTimeInstall', value);
} }
Future<UserModel> getUser() async { Future<UserModel?> getUser() async {
UserModel user = UserModel( _user = UserModel(
userId: await _secureService.getInt('userId'), userId: await _secureService.getInt('userId'),
email: await _secureService.getString('email'),
region: await _secureService.getString('region'),
gender: await _secureService.getString('gender'),
country: await _secureService.getString('country'),
lastName: await _secureService.getString('lastName'),
birthday: await _secureService.getString('birthday'),
firstName: await _secureService.getString('firstName'),
occupation: await _secureService.getString('occupation'),
accessToken: await _secureService.getString('accessToken'), accessToken: await _secureService.getString('accessToken'),
refreshToken: await _secureService.getString('refreshToken'), refreshToken: await _secureService.getString('refreshToken'),
profilePicture: await _secureService.getString('profileImage'),
userInfoLoaded: await _secureService.getBool('userInfoLoaded'),
profileCompleted: await _secureService.getBool('profileCompleted'), profileCompleted: await _secureService.getBool('profileCompleted'),
); );
return user; return _user;
} }
Future<void> logOut() async { Future<void> logOut() async {
bool firstTimeInstall = await isFirstTimeInstall(); bool firstTimeInstall = await isFirstTimeInstall();
_user = null;
await _secureService.clear(); await _secureService.clear();
await setFirstTimeInstall(firstTimeInstall); await setFirstTimeInstall(firstTimeInstall);
} }

View File

@ -4,7 +4,6 @@ import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user_model.dart'; import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart'; import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
import '../app/app.locator.dart'; import '../app/app.locator.dart';
import '../ui/common/app_constants.dart'; import '../ui/common/app_constants.dart';
@ -21,7 +20,7 @@ class DioService {
DioService() { DioService() {
_dio.options _dio.options
..baseUrl = baseUrl ..baseUrl = kBaseUrl
..connectTimeout = const Duration(seconds: 30) ..connectTimeout = const Duration(seconds: 30)
..receiveTimeout = const Duration(seconds: 30); ..receiveTimeout = const Duration(seconds: 30);
@ -50,10 +49,11 @@ class DioService {
RequestOptions options, RequestOptions options,
RequestInterceptorHandler handler, RequestInterceptorHandler handler,
) async { ) async {
final token = await _authenticationService.getAccessToken(); final access = await _authenticationService.getAccessToken();
final refresh = await _authenticationService.getRefreshToken();
if (token != null) { if (access != null) {
options.headers['Authorization'] = 'Bearer $token'; options.headers['Authorization'] = 'Bearer $access';
} }
options.headers['Accept'] = 'application/json'; options.headers['Accept'] = 'application/json';
@ -61,6 +61,7 @@ class DioService {
debugPrint('INITIALIZING REQUEST➡'); debugPrint('INITIALIZING REQUEST➡');
debugPrint('➡️ ${options.method} ${options.uri}'); debugPrint('➡️ ${options.method} ${options.uri}');
debugPrint('➡️ REFRESH: $refresh');
debugPrint('➡️ HEADERS: ${options.headers}'); debugPrint('➡️ HEADERS: ${options.headers}');
debugPrint('➡️ DATA: ${options.data}'); debugPrint('➡️ DATA: ${options.data}');
debugPrint('FINALIZING REQUEST➡'); debugPrint('FINALIZING REQUEST➡');
@ -125,33 +126,22 @@ class DioService {
} }
Future<bool> _refreshToken() async { Future<bool> _refreshToken() async {
final UserModel user = await _authenticationService.getUser(); final UserModel? user = await _authenticationService.getUser();
if (user.refreshToken == null) return false; if (user?.refreshToken == null) return false;
try { try {
Map<String, dynamic> data = { Map<String, dynamic> data = {
'role': 'STUDENT', 'role': 'USER',
'user_id': user.userId, 'user_id': user?.userId,
'access_token': user.accessToken, 'access_token': user?.accessToken,
'refresh_token': user.refreshToken 'refresh_token': user?.refreshToken
}; };
print(data);
final response = await _refreshDio.post( final response = await _refreshDio.post(
'$baseUrl/$kRefreshTokenUrl', '$kBaseUrl/$kRefreshTokenUrl',
data: data, data: data,
options: Options(
followRedirects: false,
validateStatus: (status) => true,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
); );
print('Refresh response');
print(response.data);
await _authenticationService.saveTokens( await _authenticationService.saveTokens(
access: response.data['access_token'], access: response.data['access_token'],
refresh: response.data['refresh_token'], refresh: response.data['refresh_token'],
@ -159,10 +149,8 @@ class DioService {
return true; return true;
} catch (e) { } catch (e) {
print('Refresh response exception'); await _authenticationService.logOut();
print(e.toString()); await _navigationService.replaceWithLoginView();
// await _authenticationService.logOut();
// await _navigationService.replaceWithLoginView();
return false; return false;
} }
} }

View File

@ -0,0 +1,21 @@
import 'package:google_sign_in/google_sign_in.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
class GoogleAuthService {
final GoogleSignIn signIn = GoogleSignIn.instance;
Future<GoogleSignInAccount?> googleAuth() async {
try {
GoogleSignInAccount? googleUser;
await signIn.initialize(serverClientId: kServerClientId).then((_) async {
googleUser = await signIn.attemptLightweightAuthentication();
googleUser ??=
await signIn.authenticate(scopeHint: ['email', 'profile']);
});
return googleUser;
} catch (e) {
return null;
}
}
}

View File

@ -0,0 +1,41 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import '../app/app.locator.dart';
import '../ui/common/app_constants.dart';
import 'dio_service.dart';
class ImageDownloaderService {
final _service = locator<DioService>();
Future<String> downloader(String? networkImage) async {
late File image;
late String profileImage;
final Directory appDir = await getApplicationDocumentsDirectory();
if (networkImage != null) {
profileImage = networkImage.contains('https://lh3.googleusercontent.com')
? networkImage
: '$kBaseUrl$networkImage';
}
final Response profileImageResponse = await _service.dio.get(
profileImage,
options: Options(
followRedirects: false,
responseType: ResponseType.bytes,
),
);
final imageName = basename(networkImage ?? '');
final localImagePath = join(appDir.path, imageName);
image = File(localImagePath);
image.writeAsBytes(profileImageResponse.data);
return image.path;
}
}

View File

@ -0,0 +1,56 @@
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:yimaru_app/services/permission_handler_service.dart';
import '../app/app.locator.dart';
import '../ui/common/ui_helpers.dart';
class ImagePickerService {
final _permissionHandler = locator<PermissionHandlerService>();
final ImagePicker _picker = ImagePicker();
Future<String?> gallery() async {
try {
PermissionStatus status =
await _permissionHandler.requestPermission(Permission.mediaLibrary);
if (status == PermissionStatus.granted) {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery, maxWidth: 600, maxHeight: 600);
if (pickedFile == null) {
showErrorToast('Please select a picture');
return null;
} else {
return pickedFile.path;
}
}
return null;
} catch (e) {
return null;
}
}
Future<String?> camera() async {
try {
PermissionStatus status =
await _permissionHandler.requestPermission(Permission.camera);
if (status == PermissionStatus.granted) {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.camera, maxWidth: 600, maxHeight: 600);
if (pickedFile == null) {
showErrorToast('Please take a picture');
return null;
} else {
return pickedFile.path;
}
}
return null;
} catch (e) {
return null;
}
}
}

View File

@ -0,0 +1,31 @@
import 'package:permission_handler/permission_handler.dart';
import '../ui/common/ui_helpers.dart';
class PermissionHandlerService {
Future<PermissionStatus> requestPermission(
Permission requestedPermission) async {
if (requestedPermission == Permission.camera) {
return await request(Permission.camera);
}
if (requestedPermission == Permission.storage) {
return await request(Permission.storage);
}
if (requestedPermission == Permission.mediaLibrary) {
return await request(Permission.mediaLibrary);
}
return PermissionStatus.denied;
}
Future<PermissionStatus> request(Permission permission) async {
if (await permission.isDenied) {
final PermissionStatus status = await permission.request();
if (status.isDenied || status.isPermanentlyDenied) {
showErrorToast('Permission Denied');
}
return status;
}
return PermissionStatus.granted;
}
}

View File

@ -5,6 +5,7 @@ const Color kcRed = Color(0xffFF4C4C);
const Color kcGreen = Color(0xFF1DE964); const Color kcGreen = Color(0xFF1DE964);
const Color kcBackgroundColor = kcWhite; const Color kcBackgroundColor = kcWhite;
const Color kcWhite = Color(0xFFFFFFFF); const Color kcWhite = Color(0xFFFFFFFF);
const Color kcViolet = Color(0x336A1B9A);
const Color kcIndigo = Color(0xff6A1B9A); const Color kcIndigo = Color(0xff6A1B9A);
const Color kcOrange = Color(0xFFF79400); const Color kcOrange = Color(0xFFF79400);
const Color kcSkyBlue = Color(0xFF28B4CD); const Color kcSkyBlue = Color(0xFF28B4CD);
@ -13,7 +14,6 @@ const Color kcMediumGrey = Color(0xFF474A54);
const Color kcAquamarine = Color(0xFF1DE9B6); const Color kcAquamarine = Color(0xFF1DE9B6);
const Color kcTransparent = Colors.transparent; const Color kcTransparent = Colors.transparent;
const Color kcPrimaryColor = Color(0xFF9E2891); const Color kcPrimaryColor = Color(0xFF9E2891);
const Color kcPrimaryAccent = Color(0xFF6A1B9A);
const Color kcVeryLightGrey = Color(0xFFE3E3E3); const Color kcVeryLightGrey = Color(0xFFE3E3E3);
const Color kcPrimaryColorDark = Color(0xFF300151); const Color kcPrimaryColorDark = Color(0xFF300151);
const Color kcPrimaryColorLight = Color(0x149E2891); const Color kcPrimaryColorLight = Color(0x149E2891);

View File

@ -1,7 +1,9 @@
String baseUrl = 'http://195.35.29.82:8080'; String kBaseUrl = 'http://195.35.29.82:8080';
//String baseUrl = 'https://api.yimaru.yaltopia.com'; //String baseUrl = 'https://api.yimaru.yaltopia.com';
String userUrl = 'api/v1/user'; String kGetUserUrl = 'single';
String kUserUrl = 'api/v1/user';
String kRegisterUrl = 'register'; String kRegisterUrl = 'register';
@ -9,10 +11,24 @@ String kVerifyOtpUrl = 'verify-otp';
String kResendOtpUrl = 'resend-otp'; String kResendOtpUrl = 'resend-otp';
String kResetPassword = 'resetPassword';
String kRequestResetCode = 'sendResetCode';
String kUpdateProfileImage = 'profile-picture';
String kRefreshTokenUrl = 'api/v1/auth/refresh'; String kRefreshTokenUrl = 'api/v1/auth/refresh';
String kLoginUrl = 'api/v1/auth/customer-login'; String kLoginUrl = 'api/v1/auth/customer-login';
String kProfileStatusUrl = 'is-profile-completed'; String kProfileStatusUrl = 'is-profile-completed';
String kGoogleAuthUrl = 'api/v1/auth/google/android';
String kAssessmentsUrl = 'api/v1/assessment/questions'; String kAssessmentsUrl = 'api/v1/assessment/questions';
String kServerClientId =
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
String kSampleVideoUrl =
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';

View File

@ -8,3 +8,19 @@ enum ProgressStatuses { pending, started, completed }
// Levels // Levels
enum ProficiencyLevels { a1, a2, b1, b2, none } enum ProficiencyLevels { a1, a2, b1, b2, none }
// State object
enum StateObjects {
verifyOtp,
resendOtp,
profileImage,
profileUpdate,
resetPassword,
loginWithEmail,
loginWithGoogle,
loadLessonVideo,
requestResetCode,
registerWithEmail,
profileCompletion,
registerWithGoogle,
}

View File

@ -1,4 +1,5 @@
import 'dart:math'; import 'dart:math';
import 'package:chewie/chewie.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/flutter_html.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -171,30 +172,64 @@ PinTheme errorPinTheme = defaultPin.copyBorderWith(
border: Border.all(color: Colors.red), border: Border.all(color: Colors.red),
); );
TextStyle validationStyle = const TextStyle( TextStyle style18P600 = const TextStyle(
fontSize: 18,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style18W600 = const TextStyle(
fontSize: 18,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style25W600 = const TextStyle(
fontSize: 25,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.red, color: Colors.red,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
); );
TextStyle style14P400 = const TextStyle(
color: kcPrimaryColor,
);
TextStyle style14P600 = const TextStyle(
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style25P600 = const TextStyle(
fontSize: 25,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
);
TextStyle style25DG600 = const TextStyle( TextStyle style25DG600 = const TextStyle(
fontSize: 25, fontSize: 25,
color: kcDarkGrey, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style12R700 = const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
);
TextStyle style16DG600 = const TextStyle( TextStyle style16DG600 = const TextStyle(
fontSize: 16, fontSize: 16,
color: kcDarkGrey, color: kcDarkGrey,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
); );
TextStyle style18DG500 = const TextStyle(
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w500,
);
TextStyle style18DG600 = const TextStyle( TextStyle style18DG600 = const TextStyle(
fontSize: 18, fontSize: 18,
color: kcDarkGrey, color: kcDarkGrey,
@ -206,17 +241,29 @@ TextStyle style16DG400 = const TextStyle(
color: kcDarkGrey, color: kcDarkGrey,
); );
TextStyle style14LG400 = const TextStyle(
color: kcLightGrey,
);
TextStyle style14MG400 = const TextStyle(
color: kcMediumGrey,
);
TextStyle style14DG500 =
const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500);
TextStyle style14DG400 = const TextStyle( TextStyle style14DG400 = const TextStyle(
color: kcDarkGrey, color: kcDarkGrey,
); );
TextStyle style14P400 = const TextStyle( TextStyle style14DG600 = const TextStyle(
color: kcPrimaryColor, color: kcDarkGrey,
fontWeight: FontWeight.w600,
); );
TextStyle style14P600 = const TextStyle( TextStyle validationStyle = const TextStyle(
color: kcPrimaryColor, fontSize: 12,
fontWeight: FontWeight.w600, color: Colors.red,
fontWeight: FontWeight.w700,
); );
Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16)); Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16));
@ -240,27 +287,36 @@ Map<String, Style> htmlStyle = {
), ),
}; };
ChewieProgressColors buildChewieProgressIndicator = ChewieProgressColors(
bufferedColor: kcIndigo,
playedColor: kcPrimaryColor,
backgroundColor: kcBackgroundColor,
);
Widget buildToastDescription(String message) => Text( Widget buildToastDescription(String message) => Text(
message, message,
maxLines: 4, maxLines: 4,
style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500), style: const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500),
); );
void showErrorToast(String message) { void showErrorToast(String message) {
toastification.show( toastification.show(
showIcon: true, showIcon: true,
dragToClose: true, dragToClose: true,
primaryColor: kcRed,
showProgressBar: false, showProgressBar: false,
applyBlurEffect: false, applyBlurEffect: false,
icon: const Icon(Icons.check), alignment: Alignment.topCenter,
primaryColor: kcBackgroundColor,
type: ToastificationType.success, type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored, style: ToastificationStyle.fillColored,
description: buildToastDescription(message), description: buildToastDescription(message),
borderSide: const BorderSide(color: kcWhite), autoCloseDuration: const Duration(seconds: 3),
autoCloseDuration: const Duration(seconds: 5),
margin: const EdgeInsets.symmetric(horizontal: 15), margin: const EdgeInsets.symmetric(horizontal: 15),
borderSide: const BorderSide(color: kcPrimaryColor),
icon: const Icon(
Icons.close,
color: kcPrimaryColor,
),
); );
} }
@ -270,14 +326,17 @@ void showSuccessToast(String message) {
dragToClose: true, dragToClose: true,
showProgressBar: false, showProgressBar: false,
applyBlurEffect: false, applyBlurEffect: false,
icon: const Icon(Icons.check), alignment: Alignment.topCenter,
primaryColor: kcPrimaryColor, primaryColor: kcBackgroundColor,
type: ToastificationType.success, type: ToastificationType.success,
alignment: Alignment.bottomCenter,
style: ToastificationStyle.fillColored, style: ToastificationStyle.fillColored,
description: buildToastDescription(message), description: buildToastDescription(message),
borderSide: const BorderSide(color: kcWhite), autoCloseDuration: const Duration(seconds: 3),
autoCloseDuration: const Duration(seconds: 5),
margin: const EdgeInsets.symmetric(horizontal: 15), margin: const EdgeInsets.symmetric(horizontal: 15),
borderSide: const BorderSide(color: kcPrimaryColor),
icon: const Icon(
Icons.check,
color: kcPrimaryColor,
),
); );
} }

View File

@ -107,11 +107,7 @@ class AccountPrivacyView extends StackedView<AccountPrivacyViewModel> {
Widget _buildHeader(String title) => Text( Widget _buildHeader(String title) => Text(
title, title,
style: const TextStyle( style: style18DG600,
fontSize: 18,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) => Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) =>

View File

@ -17,9 +17,9 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
const AssessmentView({Key? key, required this.data}) : super(key: key); const AssessmentView({Key? key, required this.data}) : super(key: key);
@override @override
void onViewModelReady(AssessmentViewModel viewModel) { void onViewModelReady(AssessmentViewModel viewModel) async {
viewModel.getAssessments();
viewModel.initUserData(data); viewModel.initUserData(data);
await viewModel.getAssessments();
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -39,10 +39,12 @@ class AssessmentView extends StackedView<AssessmentViewModel> {
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildAssessmentIntro(), _buildAssessmentIntro(),
_buildAssessment(), _buildAssessment(),
// _buildAssessmentFailure(), /*
// _buildRetakeAssessment(), _buildAssessmentFailure(),
// _buildResultAnalysis(), _buildRetakeAssessment(),
// _buildAssessmentCompletion(), _buildResultAnalysis(),
_buildAssessmentCompletion(),
*/
_buildAssessmentResult(), _buildAssessmentResult(),
_buildStartLesson(), _buildStartLesson(),
]; ];

View File

@ -3,21 +3,25 @@ import 'dart:math';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/models/option.dart';
import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../app/app.router.dart'; import '../../../app/app.router.dart';
import '../../../models/assessment.dart'; import '../../../models/assessment.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../home/home_view.dart'; import '../home/home_view.dart';
class AssessmentViewModel extends BaseViewModel { class AssessmentViewModel extends BaseViewModel {
final _apiService = locator<ApiService>(); final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
// In-app navigation
int _currentPage = 0; int _currentPage = 0;
int get currentPage => _currentPage; int get currentPage => _currentPage;
@ -31,7 +35,6 @@ class AssessmentViewModel extends BaseViewModel {
int get previousPage => _previousPage; int get previousPage => _previousPage;
// Assessment // Assessment
int _currentQuestion = 0; int _currentQuestion = 0;
int get currentQuestion => _currentQuestion; int get currentQuestion => _currentQuestion;
@ -54,7 +57,6 @@ class AssessmentViewModel extends BaseViewModel {
Map<String, dynamic> get userData => _userData; Map<String, dynamic> get userData => _userData;
// Assessment // Assessment
int countCorrectAnswersUntil(int untilQuestion) { int countCorrectAnswersUntil(int untilQuestion) {
int count = 0; int count = 0;
@ -73,9 +75,6 @@ class AssessmentViewModel extends BaseViewModel {
if (_currentQuestion == 5) { if (_currentQuestion == 5) {
// A1 // A1
final correctCount = countCorrectAnswersUntil(5); final correctCount = countCorrectAnswersUntil(5);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct A1: $correctCount');
if (correctCount > 3) { if (correctCount > 3) {
return {'continue': true, 'level': ProficiencyLevels.a1}; return {'continue': true, 'level': ProficiencyLevels.a1};
@ -86,9 +85,6 @@ class AssessmentViewModel extends BaseViewModel {
// A2 // A2
final correctCount = countCorrectAnswersUntil(10); final correctCount = countCorrectAnswersUntil(10);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct A2: $correctCount');
if (correctCount > 3) { if (correctCount > 3) {
return {'continue': true, 'level': ProficiencyLevels.a2}; return {'continue': true, 'level': ProficiencyLevels.a2};
@ -98,9 +94,6 @@ class AssessmentViewModel extends BaseViewModel {
} else if (_currentQuestion == 16) { } else if (_currentQuestion == 16) {
// B1 // B1
final correctCount = countCorrectAnswersUntil(16); final correctCount = countCorrectAnswersUntil(16);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct B1: $correctCount');
if (correctCount > 4) { if (correctCount > 4) {
return {'continue': true, 'level': ProficiencyLevels.b1}; return {'continue': true, 'level': ProficiencyLevels.b1};
@ -109,12 +102,9 @@ class AssessmentViewModel extends BaseViewModel {
} }
} else if (_currentQuestion == 22) { } else if (_currentQuestion == 22) {
final correctCount = countCorrectAnswersUntil(16); final correctCount = countCorrectAnswersUntil(16);
print('All : $_selectedAnswers');
print('Question page : $_currentQuestion');
print('Correct B2: $correctCount');
if (correctCount > 4) { if (correctCount > 4) {
return {'continue': true, 'level': ProficiencyLevels.b2}; return {'continue': false, 'level': ProficiencyLevels.b2};
} else { } else {
return {'continue': false, 'level': ProficiencyLevels.b2}; return {'continue': false, 'level': ProficiencyLevels.b2};
} }
@ -123,18 +113,20 @@ class AssessmentViewModel extends BaseViewModel {
} }
} }
void setSelectedAnswer({required int question, required String option}) { void setSelectedAnswer({required int question, required Option? option}) {
bool correct = false; bool correct = false;
final generator = Random(); if (option?.isCorrect ?? false) {
int random = generator.nextInt(4);
if (option == _assessments[question - 1].options?[random].optionText) {
correct = true; correct = true;
} }
final data = { final data = {
question.toString(): { question.toString(): {
'option': option,
'correct': correct, 'correct': correct,
'answer': _assessments[question - 1].options?[random].optionText 'option': option?.optionText,
'answer': _assessments[question - 1]
.options
?.firstWhere((e) => e.isCorrect ?? false)
.optionText
} }
}; };
@ -147,22 +139,6 @@ class AssessmentViewModel extends BaseViewModel {
return _selectedAnswers[question.toString()]?['option'] == answer; return _selectedAnswers[question.toString()]?['option'] == answer;
} }
Future<void> getAssessments() async {
_assessments = await runBusyFuture<List<Assessment>>(_getAssessments());
}
Future<List<Assessment>> _getAssessments() async {
List<Assessment> response = await _apiService.getAssessments();
for (int i = 0; i < 6; i++) {
final generator = Random();
int random = generator.nextInt(15);
response.add(response[random]);
}
return response;
}
// Add user data // Add user data
void initUserData(Map<String, dynamic> data) { void initUserData(Map<String, dynamic> data) {
clearUserData(); clearUserData();
@ -177,38 +153,48 @@ class AssessmentViewModel extends BaseViewModel {
_userData.clear(); _userData.clear();
} }
// Complete profile // Dialog
Future<void> completeProfile() async { Future<bool?> showAbortDialog() async {
Map<String, dynamic> response = DialogResponse? response = await _dialogService.showDialog(
await runBusyFuture<Map<String, dynamic>>(_completeProfile()); cancelTitle: 'No',
buttonTitle: 'Yes',
barrierDismissible: true,
title: 'Abort Assessment',
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure to abort the assessment ?',
);
return response?.confirmed;
} }
Future<Map<String, dynamic>> _completeProfile() async { Future<void> abort() async {
print(_userData); bool? response = await showAbortDialog();
UserModel user = await _authenticationService.getUser(); if (response != null && response) {
Map<String, dynamic> response = next(page: 3);
await _apiService.updateProfile(data: _userData, user: user); }
return response;
} }
// Navigation // Question navigation
void nextQuestion() { void nextQuestion() {
_currentQuestion++; _currentQuestion++;
Map<String, dynamic> response = evaluateAssessment(); Map<String, dynamic> response = evaluateAssessment();
if (response['level'] == ProficiencyLevels.none) { if (_currentQuestion == _assessments.length) {
_pageController.jumpToPage(_currentQuestion); _proficiencyLevel = response['level'];
next();
} else { } else {
if (response['continue']) { if (response['level'] == ProficiencyLevels.none) {
_pageController.jumpToPage(_currentQuestion); _pageController.jumpToPage(_currentQuestion);
} } else {
{ if (response['continue']) {
_proficiencyLevel = response['level']; _pageController.jumpToPage(_currentQuestion);
next(); } else {
_proficiencyLevel = response['level'];
next();
}
} }
} }
rebuildUi(); rebuildUi();
} }
@ -218,8 +204,6 @@ class AssessmentViewModel extends BaseViewModel {
_pageController.previousPage( _pageController.previousPage(
duration: const Duration(microseconds: 100), curve: Curves.linear); duration: const Duration(microseconds: 100), curve: Curves.linear);
rebuildUi(); rebuildUi();
} else {
_navigationService.back();
} }
} }
@ -238,15 +222,54 @@ class AssessmentViewModel extends BaseViewModel {
} }
void pop() { void pop() {
if (_currentPage != 0) { if (_currentPage == 0 || _currentPage == 3 /*7*/) {
_navigationService.back();
} else if (_currentPage != 0 && _currentPage != 3) {
_currentPage--; _currentPage--;
rebuildUi(); rebuildUi();
} }
} }
// Navigation
Future<void> navigateToLanguage() async => Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView(); await _navigationService.navigateToLanguageView();
Future<void> replaceWithHome() async => Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView()); await _navigationService.clearStackAndShowView(const HomeView());
// Remote api call
Future<void> getAssessments() async => await runBusyFuture(_getAssessments());
Future<void> _getAssessments() async {
if (await _statusChecker.checkConnection()) {
List<Assessment> response = await _apiService.getAssessments();
/*
for (int i = 0; i < 6; i++) {
final generator = Random();
int random = generator.nextInt(15);
response.add(response[random]);
}
*/
_assessments = response;
}
}
// Complete profile
Future<void> completeProfile() async =>
await runBusyFuture(_completeProfile(),
busyObject: StateObjects.profileCompletion);
Future<void> _completeProfile() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.completeProfile(_userData);
if (response['status'] == ResponseStatus.success) {
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
} }

View File

@ -61,27 +61,23 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset( Widget _buildIcon() => SvgPicture.asset(
'assets/icons/complete.svg', 'assets/icons/complete.svg',
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Assessment complete!', 'Assessment complete!',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Were now analyzing your speaking skills', 'Were now analyzing your speaking skills',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
@ -94,8 +90,8 @@ class AssessmentCompletionScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'View My Results', text: 'View My Results',
onTap: () => viewModel.next(),
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
} }

View File

@ -64,25 +64,21 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg'); Widget _buildIcon() => SvgPicture.asset('assets/icons/alert.svg');
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'We didnt get enough from your assessment', 'We didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ', 'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -117,9 +113,9 @@ class AssessmentFailureScreen extends ViewModelWidget<AssessmentViewModel> {
height: 55, height: 55,
text: 'Skip', text: 'Skip',
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -12,18 +12,21 @@ import 'assessment_loading_screen.dart';
class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> { class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
const AssessmentFormScreen({super.key}); const AssessmentFormScreen({super.key});
//final PageController _pageController = PageController();
@override @override
Widget build(BuildContext context, AssessmentViewModel viewModel) => Widget build(BuildContext context, AssessmentViewModel viewModel) =>
_buildAssessmentScreens(viewModel); _buildAssessmentScreens(viewModel);
Widget _buildAssessmentScreens(AssessmentViewModel viewModel) => Widget _buildAssessmentScreens(AssessmentViewModel viewModel) =>
viewModel.isBusy viewModel.isBusy || viewModel.assessments.isEmpty
? _buildPageLoadingIndicator() ? _buildPageLoadingIndicator(viewModel)
: _buildAssessmentScreensWrapper(viewModel); : _buildAssessmentScreensWrapper(viewModel);
Widget _buildPageLoadingIndicator() => const AssessmentLoadingScreen(); Widget _buildPageLoadingIndicator(AssessmentViewModel viewModel) =>
AssessmentLoadingScreen(
isLoading: viewModel.isBusy,
isEmpty: viewModel.assessments.isEmpty,
onTap: () async => await viewModel.getAssessments(),
);
Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) => Widget _buildAssessmentScreensWrapper(AssessmentViewModel viewModel) =>
PopScope( PopScope(
@ -45,9 +48,10 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: true, onClose: viewModel.abort,
showLanguageSelection: false, showLanguageSelection: false,
onPop: viewModel.previousQuestion, onPop: viewModel.previousQuestion,
showBackButton: viewModel.currentQuestion == 0 ? false : true,
); );
Widget _buildExpandedBody(AssessmentViewModel viewModel) => Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
@ -59,10 +63,11 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
); );
Widget _buildAssessment(AssessmentViewModel viewModel) => PageView.builder( Widget _buildAssessment(AssessmentViewModel viewModel) => PageView.builder(
controller: viewModel.pageController, controller: viewModel.pageController,
itemCount: viewModel.assessments.length, itemCount: viewModel.assessments.length,
itemBuilder: (cotext, index) => itemBuilder: (cotext, index) =>
_buildBody(index: index, viewModel: viewModel)); _buildBody(index: index, viewModel: viewModel),
);
Widget _buildBody( Widget _buildBody(
{required int index, required AssessmentViewModel viewModel}) => {required int index, required AssessmentViewModel viewModel}) =>
@ -105,7 +110,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildTitle( Widget _buildTitle(
{required int index, required AssessmentViewModel viewModel}) => {required int index, required AssessmentViewModel viewModel}) =>
Text( Text(
'Q${index + 1}. ${viewModel.assessments[index].question?.title} ', 'Q${index + 1}. ${viewModel.assessments[index].questionText} ',
style: style16DG600, style: style16DG600,
); );
@ -123,8 +128,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
''), ''),
onTap: () => viewModel.setSelectedAnswer( onTap: () => viewModel.setSelectedAnswer(
question: index + 1, question: index + 1,
option: viewModel.assessments[index].options?[inner].optionText ?? option: viewModel.assessments[index].options?[inner]),
''),
), ),
); );
@ -159,11 +163,7 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
? kcPrimaryColor ? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1), : kcPrimaryColor.withOpacity(0.1),
onTap: viewModel.selectedAnswers.containsKey(question.toString()) onTap: viewModel.selectedAnswers.containsKey(question.toString())
? ? () => viewModel.nextQuestion()
// viewModel.currentQuestion == viewModel.assessments.length - 1
// ? () => viewModel.next()
// :
() => viewModel.nextQuestion()
: null, : null,
); );
} }

View File

@ -54,27 +54,24 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: false, showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true, showLanguageSelection: true,
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Want a quick assessment to know your English level?', 'Want a quick assessment to know your English level?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Answer a few quick questions to help us understand your English proficiency.', 'Answer a few quick questions to help us understand your English proficiency.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -94,8 +91,8 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
safe: false, safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
onTap: () => viewModel.next(),
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );

View File

@ -3,9 +3,14 @@ import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../widgets/large_app_bar.dart'; import '../../../widgets/large_app_bar.dart';
import '../../../widgets/refresh_button.dart';
class AssessmentLoadingScreen extends StatelessWidget { class AssessmentLoadingScreen extends StatelessWidget {
const AssessmentLoadingScreen({super.key}); final bool isEmpty;
final bool isLoading;
final GestureTapCallback? onTap;
const AssessmentLoadingScreen(
{super.key, this.onTap, required this.isEmpty, required this.isLoading});
@override @override
Widget build(BuildContext context) => _buildScaffoldWrapper(); Widget build(BuildContext context) => _buildScaffoldWrapper();
@ -16,7 +21,11 @@ class AssessmentLoadingScreen extends StatelessWidget {
); );
Widget _buildScaffold() => Stack( Widget _buildScaffold() => Stack(
children: [_buildColumn(), _buildPageIndicator()], children: [
_buildColumn(),
if (isEmpty) _buildRefreshButton(),
if (isLoading) _buildPageIndicator()
],
); );
Widget _buildColumn() => Column( Widget _buildColumn() => Column(
@ -34,4 +43,6 @@ class AssessmentLoadingScreen extends StatelessWidget {
Widget _buildBody() => Expanded(child: Container()); Widget _buildBody() => Expanded(child: Container());
Widget _buildPageIndicator() => const PageLoadingIndicator(); Widget _buildPageIndicator() => const PageLoadingIndicator();
Widget _buildRefreshButton() => RefreshButton(onTap: onTap);
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.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/app_colors.dart'; import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.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/custom_elevated_button.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
@ -62,35 +63,37 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceLarge, verticalSpaceLarge,
_buildTitle(viewModel), _buildTitle(viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildPrimarySubTitle(), _buildPrimarySubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildIcon(), _buildIconWrapper(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildSecondarySubTitle() _buildSecondarySubtitle()
]; ];
Widget _buildTitle(AssessmentViewModel viewModel) => Text( Widget _buildTitle(AssessmentViewModel viewModel) => Text(
'Youre likely a ${viewModel.proficiencyLevel.name.toUpperCase()} speaker!', 'Youre likely a ${viewModel.proficiencyLevel.name.toUpperCase()} speaker!',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 25,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
); );
Widget _buildPrimarySubTitle() => const Text( Widget _buildPrimarySubtitle() => Text(
'Great Job! Heres your next step to keep improving.', 'Great Job! Heres your next step to keep improving.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildIcon() => SvgPicture.asset('assets/icons/b1.svg'); Widget _buildIconWrapper(AssessmentViewModel viewModel) =>
viewModel.proficiencyLevel != ProficiencyLevels.none
? _buildIcon(viewModel)
: Container();
Widget _buildSecondarySubTitle() => const Text( Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset(
'assets/icons/${viewModel.proficiencyLevel.name.substring(0, 1)}_${viewModel.proficiencyLevel.name.substring(1)}.svg');
Widget _buildSecondarySubtitle() => Text(
'Let\'s start your practice', 'Let\'s start your practice',
style: style14DG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
); );
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(
@ -110,8 +113,8 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
safe: false, safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
onTap: () => viewModel.next(),
foregroundColor: kcWhite, foregroundColor: kcWhite,
onTap: () => viewModel.next(),
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
); );
@ -124,10 +127,10 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite,
text: 'Practice Speaking', text: 'Practice Speaking',
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.next(), onTap: () => viewModel.next(),
backgroundColor: kcWhite,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
} }

View File

@ -48,7 +48,7 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -61,19 +61,15 @@ class ResultAnalysisScreen extends ViewModelWidget<AssessmentViewModel> {
'assets/icons/progress_indicator.svg', 'assets/icons/progress_indicator.svg',
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Analyzing your results…', 'Analyzing your results…',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Were now analyzing your speaking skills', 'Were now analyzing your speaking skills',
style: style14MG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
); );
} }

View File

@ -57,7 +57,7 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
@ -72,20 +72,16 @@ class RetakeAssessmentScreen extends ViewModelWidget<AssessmentViewModel> {
color: kcPrimaryColor, color: kcPrimaryColor,
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'We didnt get enough from your assessment', 'We didnt get enough from your assessment',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ', 'Your assessment wasnt long enough for us to analyze your speaking level. You can retake the call to get accurate results ',
style: style14MG400,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey),
); );
Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column( Widget _buildLowerColumn(AssessmentViewModel viewModel) => Column(

View File

@ -7,6 +7,7 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart';
import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
import '../../../common/enmus.dart'; import '../../../common/enmus.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../assessment_viewmodel.dart'; import '../assessment_viewmodel.dart';
class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> { class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
@ -15,7 +16,6 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
Future<void> _start(AssessmentViewModel viewModel) async { Future<void> _start(AssessmentViewModel viewModel) async {
if (viewModel.proficiencyLevel != ProficiencyLevels.none) { if (viewModel.proficiencyLevel != ProficiencyLevels.none) {
Map<String, dynamic> data = { Map<String, dynamic> data = {
'preferred_language': 'en',
'knowledge_level': viewModel.proficiencyLevel.name.toUpperCase() 'knowledge_level': viewModel.proficiencyLevel.name.toUpperCase()
}; };
@ -31,20 +31,24 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(AssessmentViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffoldStack(viewModel),
); );
Widget _buildScaffoldStack(AssessmentViewModel viewModel) =>
Stack(children: [_buildScaffold(viewModel), _buildState(viewModel)]);
Widget _buildScaffold(AssessmentViewModel viewModel) => Column( Widget _buildScaffold(AssessmentViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel), children: _buildScaffoldChildren(viewModel),
); );
List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) => List<Widget> _buildScaffoldChildren(AssessmentViewModel viewModel) =>
[_buildAppBar(), _buildExpandedBody(viewModel)]; [_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar() => const LargeAppBar( Widget _buildAppBar(AssessmentViewModel viewModel) => LargeAppBar(
showBackButton: false, showBackButton: true,
showLanguageSelection: false, onPop: viewModel.pop,
showLanguageSelection: true,
); );
Widget _buildExpandedBody(AssessmentViewModel viewModel) => Widget _buildExpandedBody(AssessmentViewModel viewModel) =>
@ -76,34 +80,27 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(viewModel), _buildTitle(viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
]; ];
Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg'); Widget _buildIcon() => SvgPicture.asset('assets/icons/mascot.svg');
Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich( Widget _buildTitle(AssessmentViewModel viewModel) => Text.rich(
TextSpan( TextSpan(
text: 'Welcome aboard', text: 'Welcome aboard',
style: const TextStyle( style: style25DG600,
fontSize: 25, children: [
color: kcDarkGrey, TextSpan(
fontWeight: FontWeight.w600, style: style25DG600,
text: ', ${viewModel.userData['first_name']}!',
), ),
children: [ ],
TextSpan( ),
text: ', ${viewModel.userData['first_name']}!',
style: const TextStyle(
fontSize: 25,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
)
]),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Youre ready to explore your personalized lessons.', 'Youre ready to explore your personalized lessons.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
@ -114,10 +111,15 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
Widget _buildContinueButton(AssessmentViewModel viewModel) => Widget _buildContinueButton(AssessmentViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
text: 'Finish',
borderRadius: 12, borderRadius: 12,
text: 'Go to My Lessons',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => await _start(viewModel), onTap: () async => await _start(viewModel),
); );
Widget _buildState(AssessmentViewModel viewModel) =>
viewModel.busy(StateObjects.profileCompletion)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -92,14 +92,10 @@ class CallSupportView extends StackedView<CallSupportViewModel> {
Widget _buildIcon() => Widget _buildIcon() =>
const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor); const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor);
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Call our support team between 9 AM - 6 PM', 'Call our support team between 9 AM - 6 PM',
style: style25DG600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle(String title) => Text( Widget _buildSubTitle(String title) => Text(

View File

@ -178,7 +178,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildEmptyTitle(), _buildEmptyTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildEmptySubTitle(), _buildEmptySubtitle(),
]; ];
Widget _buildEmptyIcon() => const Icon( Widget _buildEmptyIcon() => const Icon(
@ -197,7 +197,7 @@ class DownloadsView extends StackedView<DownloadsViewModel> {
), ),
); );
Widget _buildEmptySubTitle() => const Text( Widget _buildEmptySubtitle() => const Text(
'Start by exploring your learning materials and save them for offline access.', 'Start by exploring your learning materials and save them for offline access.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: kcMediumGrey), style: TextStyle(color: kcMediumGrey),

View File

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stacked/stacked.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_circular_progress_indicator.dart';
import 'failure_viewmodel.dart';
class FailureView extends StackedView<FailureViewModel> {
final String label;
const FailureView({Key? key, required this.label}) : super(key: key);
@override
FailureViewModel viewModelBuilder(BuildContext context) => FailureViewModel();
@override
Widget builder(
BuildContext context,
FailureViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper();
Widget _buildScaffoldWrapper() => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(),
);
Widget _buildScaffold() => Stack(
children: _buildScaffoldChildren(),
);
List<Widget> _buildScaffoldChildren() => [
_buildBackground(),
_buildColumn(),
];
Widget _buildBackground() => Image.asset(
'assets/images/onboarding_1.png',
fit: BoxFit.fill,
width: double.maxFinite,
height: double.maxFinite,
);
Widget _buildColumn() => Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildUpperColumnChildren(),
);
List<Widget> _buildUpperColumnChildren() =>
[_buildIconWrapper(), _buildSafeWrapper()];
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 ...', style: const TextStyle(color: kcWhite, fontSize: 16));
Widget _buildIndicatorWrapper() => SizedBox(
width: 16,
height: 16,
child: _buildIndicator(),
);
Widget _buildIndicator() =>
const CustomCircularProgressIndicator(color: kcWhite);
Widget _buildIconWrapper() => Padding(
padding: const EdgeInsets.only(top: 100),
child: _buildIcon(),
);
Widget _buildIcon() => SvgPicture.asset(
'assets/icons/logo.svg',
height: 50,
);
}

View File

@ -0,0 +1,3 @@
import 'package:stacked/stacked.dart';
class FailureViewModel extends BaseViewModel {}

View File

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_view.form.dart';
import 'package:yimaru_app/ui/views/forget_password/screens/request_reset_code_screen.dart';
import 'package:yimaru_app/ui/views/forget_password/screens/reset_password_screen.dart';
import '../../common/app_colors.dart';
import '../../common/validators/form_validator.dart';
import '../../widgets/large_app_bar.dart';
import '../../widgets/page_loading_indicator.dart';
import 'forget_password_viewmodel.dart';
@FormView(fields: [
FormTextField(name: 'email', validator: FormValidator.validateEmail),
FormTextField(name: 'resetCode', validator: FormValidator.validateForm),
FormTextField(name: 'password', validator: FormValidator.validateForm),
FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm)
])
class ForgetPasswordView extends StackedView<ForgetPasswordViewModel>
with $ForgetPasswordView {
const ForgetPasswordView({Key? key}) : super(key: key);
void _initClearData() {
emailController.clear();
passwordController.clear();
resetCodeController.clear();
confirmPasswordController.clear();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
if (viewModel.currentPage == 0) {
emailController.clear();
viewModel.resetRequestResetCodeScreen();
} else {
passwordController.clear();
resetCodeController.clear();
confirmPasswordController.clear();
viewModel.resetResetPasswordScreen();
}
}
void _pop({required bool value, required ForgetPasswordViewModel viewModel}) {
{
if (!value) return;
_clearDataOnNavigation(viewModel);
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
}
}
@override
void onViewModelReady(ForgetPasswordViewModel viewModel) {
_initClearData();
syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel);
}
@override
ForgetPasswordViewModel viewModelBuilder(BuildContext context) =>
ForgetPasswordViewModel();
@override
Widget builder(
BuildContext context,
ForgetPasswordViewModel viewModel,
Widget? child,
) =>
_buildLoginScreensWrapper(viewModel);
Widget _buildLoginScreensWrapper(ForgetPasswordViewModel viewModel) =>
PopScope(
canPop: true,
onPopInvokedWithResult: (value, data) =>
_pop(value: value, viewModel: viewModel),
child: _buildBody(viewModel));
Widget _buildBody(ForgetPasswordViewModel viewModel) =>
IndexedStack(index: viewModel.currentPage, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildRequestCodeScreen(),
_buildResetPasswordScreen(),
];
Widget _buildRequestCodeScreen() =>
RequestCodeScreen(emailController: emailController);
Widget _buildResetPasswordScreen() => ResetPasswordScreen(
passwordController: passwordController,
resetCodeController: resetCodeController,
confirmPasswordController: confirmPasswordController);
}

View File

@ -0,0 +1,281 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// StackedFormGenerator
// **************************************************************************
// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true;
const String EmailValueKey = 'email';
const String ResetCodeValueKey = 'resetCode';
const String PasswordValueKey = 'password';
const String ConfirmPasswordValueKey = 'confirmPassword';
final Map<String, TextEditingController>
_ForgetPasswordViewTextEditingControllers = {};
final Map<String, FocusNode> _ForgetPasswordViewFocusNodes = {};
final Map<String, String? Function(String?)?>
_ForgetPasswordViewTextValidations = {
EmailValueKey: FormValidator.validateEmail,
ResetCodeValueKey: FormValidator.validateForm,
PasswordValueKey: FormValidator.validateForm,
ConfirmPasswordValueKey: FormValidator.validateForm,
};
mixin $ForgetPasswordView {
TextEditingController get emailController =>
_getFormTextEditingController(EmailValueKey);
TextEditingController get resetCodeController =>
_getFormTextEditingController(ResetCodeValueKey);
TextEditingController get passwordController =>
_getFormTextEditingController(PasswordValueKey);
TextEditingController get confirmPasswordController =>
_getFormTextEditingController(ConfirmPasswordValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get resetCodeFocusNode => _getFormFocusNode(ResetCodeValueKey);
FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey);
FocusNode get confirmPasswordFocusNode =>
_getFormFocusNode(ConfirmPasswordValueKey);
TextEditingController _getFormTextEditingController(
String key, {
String? initialValue,
}) {
if (_ForgetPasswordViewTextEditingControllers.containsKey(key)) {
return _ForgetPasswordViewTextEditingControllers[key]!;
}
_ForgetPasswordViewTextEditingControllers[key] =
TextEditingController(text: initialValue);
return _ForgetPasswordViewTextEditingControllers[key]!;
}
FocusNode _getFormFocusNode(String key) {
if (_ForgetPasswordViewFocusNodes.containsKey(key)) {
return _ForgetPasswordViewFocusNodes[key]!;
}
_ForgetPasswordViewFocusNodes[key] = FocusNode();
return _ForgetPasswordViewFocusNodes[key]!;
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) {
emailController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values
@Deprecated(
'Use syncFormWithViewModel instead.'
'This feature was deprecated after 3.1.0.',
)
void listenToFormUpdated(FormViewModel model) {
emailController.addListener(() => _updateFormData(model));
resetCodeController.addListener(() => _updateFormData(model));
passwordController.addListener(() => _updateFormData(model));
confirmPasswordController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation);
}
/// Updates the formData on the FormViewModel
void _updateFormData(FormStateHelper model, {bool forceValidate = false}) {
model.setData(
model.formValueMap
..addAll({
EmailValueKey: emailController.text,
ResetCodeValueKey: resetCodeController.text,
PasswordValueKey: passwordController.text,
ConfirmPasswordValueKey: confirmPasswordController.text,
}),
);
if (_autoTextFieldValidation || forceValidate) {
updateValidationData(model);
}
}
bool validateFormFields(FormViewModel model) {
_updateFormData(model, forceValidate: true);
return model.isFormValid;
}
/// Calls dispose on all the generated controllers and focus nodes
void disposeForm() {
// The dispose function for a TextEditingController sets all listeners to null
for (var controller in _ForgetPasswordViewTextEditingControllers.values) {
controller.dispose();
}
for (var focusNode in _ForgetPasswordViewFocusNodes.values) {
focusNode.dispose();
}
_ForgetPasswordViewTextEditingControllers.clear();
_ForgetPasswordViewFocusNodes.clear();
}
}
extension ValueProperties on FormStateHelper {
bool get hasAnyValidationMessage => this
.fieldsValidationMessages
.values
.any((validation) => validation != null);
bool get isFormValid {
if (!_autoTextFieldValidation) this.validateForm();
return !hasAnyValidationMessage;
}
String? get emailValue => this.formValueMap[EmailValueKey] as String?;
String? get resetCodeValue => this.formValueMap[ResetCodeValueKey] as String?;
String? get passwordValue => this.formValueMap[PasswordValueKey] as String?;
String? get confirmPasswordValue =>
this.formValueMap[ConfirmPasswordValueKey] as String?;
set emailValue(String? value) {
this.setData(
this.formValueMap..addAll({EmailValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(EmailValueKey)) {
_ForgetPasswordViewTextEditingControllers[EmailValueKey]?.text =
value ?? '';
}
}
set resetCodeValue(String? value) {
this.setData(
this.formValueMap..addAll({ResetCodeValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(
ResetCodeValueKey)) {
_ForgetPasswordViewTextEditingControllers[ResetCodeValueKey]?.text =
value ?? '';
}
}
set passwordValue(String? value) {
this.setData(
this.formValueMap..addAll({PasswordValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(
PasswordValueKey)) {
_ForgetPasswordViewTextEditingControllers[PasswordValueKey]?.text =
value ?? '';
}
}
set confirmPasswordValue(String? value) {
this.setData(
this.formValueMap..addAll({ConfirmPasswordValueKey: value}),
);
if (_ForgetPasswordViewTextEditingControllers.containsKey(
ConfirmPasswordValueKey)) {
_ForgetPasswordViewTextEditingControllers[ConfirmPasswordValueKey]?.text =
value ?? '';
}
}
bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false);
bool get hasResetCode =>
this.formValueMap.containsKey(ResetCodeValueKey) &&
(resetCodeValue?.isNotEmpty ?? false);
bool get hasPassword =>
this.formValueMap.containsKey(PasswordValueKey) &&
(passwordValue?.isNotEmpty ?? false);
bool get hasConfirmPassword =>
this.formValueMap.containsKey(ConfirmPasswordValueKey) &&
(confirmPasswordValue?.isNotEmpty ?? false);
bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
bool get hasResetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey]?.isNotEmpty ?? false;
bool get hasPasswordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false;
bool get hasConfirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ??
false;
String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey];
String? get resetCodeValidationMessage =>
this.fieldsValidationMessages[ResetCodeValueKey];
String? get passwordValidationMessage =>
this.fieldsValidationMessages[PasswordValueKey];
String? get confirmPasswordValidationMessage =>
this.fieldsValidationMessages[ConfirmPasswordValueKey];
}
extension Methods on FormStateHelper {
setEmailValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[EmailValueKey] = validationMessage;
setResetCodeValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ResetCodeValueKey] = validationMessage;
setPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[PasswordValueKey] = validationMessage;
setConfirmPasswordValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[ConfirmPasswordValueKey] =
validationMessage;
/// Clears text input fields on the Form
void clearForm() {
emailValue = '';
resetCodeValue = '';
passwordValue = '';
confirmPasswordValue = '';
}
/// Validates text input fields on the Form
void validateForm() {
this.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});
}
}
/// Returns the validation message for the given key
String? getValidationMessage(String key) {
final validatorForKey = _ForgetPasswordViewTextValidations[key];
if (validatorForKey == null) return null;
String? validationMessageForKey = validatorForKey(
_ForgetPasswordViewTextEditingControllers[key]!.text,
);
return validationMessageForKey;
}
/// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({
EmailValueKey: getValidationMessage(EmailValueKey),
ResetCodeValueKey: getValidationMessage(ResetCodeValueKey),
PasswordValueKey: getValidationMessage(PasswordValueKey),
ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey),
});

View File

@ -0,0 +1,231 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/views/login/login_view.dart';
import '../../../app/app.locator.dart';
import '../../../services/api_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
class ForgetPasswordViewModel extends FormViewModel {
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// User data
final Map<String, dynamic> _userData = {};
Map<String, dynamic> get userData => _userData;
// Navigation
int _currentPage = 0;
int get currentPage => _currentPage;
// Email
bool _focusEmail = false;
bool get focusEmail => _focusEmail;
// Reset code
bool _focusResetCode = false;
bool get focusResetCode => _focusResetCode;
// Password
bool _length = false;
bool get length => _length;
bool _number = false;
bool get number => _number;
bool _specialChar = false;
bool get specialChar => _specialChar;
bool _focusPassword = false;
bool get focusPassword => _focusPassword;
bool _obscurePassword = true;
bool get obscurePassword => _obscurePassword;
bool _passwordMatch = false;
bool get passwordMatch => _passwordMatch;
// Confirm password
bool _focusConfirmPassword = false;
bool get focusConfirmPassword => _focusConfirmPassword;
bool _obscureConfirmPassword = true;
bool get obscureConfirmPassword => _obscureConfirmPassword;
// Add user data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// Email
void setEmailFocus() {
_focusEmail = true;
rebuildUi();
}
// Reset code
void setResetCodeFocus() {
_focusResetCode = true;
rebuildUi();
}
// Password
void setPasswordFocus() {
_focusPassword = true;
rebuildUi();
}
void validatePassword(
{required String password, required String confirmPassword}) {
if (password.length > 8) {
_length = true;
} else {
_length = false;
}
if (RegExp(r'\d').hasMatch(password)) {
_number = true;
} else {
_number = false;
}
if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) {
_specialChar = true;
} else {
_specialChar = false;
}
if (password == confirmPassword) {
_passwordMatch = true;
} else {
_passwordMatch = false;
}
rebuildUi();
}
double validationProgress() {
int completed = 0;
if (_length) completed++;
if (_number) completed++;
if (_specialChar) completed++;
if (_passwordMatch) completed++;
return completed / 4; // returns 0.0 1.0
}
void setObscurePassword() {
_obscurePassword = !_obscurePassword;
rebuildUi();
}
// Confirm password
void setConfirmPasswordFocus() {
_focusConfirmPassword = true;
rebuildUi();
}
void setObscureConfirmPassword() {
_obscureConfirmPassword = !_obscureConfirmPassword;
rebuildUi();
}
// Form reset
// Reset reset password screen
void resetResetPasswordScreen() {
_length = false;
_number = false;
_specialChar = false;
_passwordMatch = false;
_focusPassword = false;
_focusResetCode = false;
_focusConfirmPassword = false;
rebuildUi();
}
// Reset reset password screen
void resetRequestResetCodeScreen() {
_focusEmail = false;
rebuildUi();
}
// In-app navigation
void goTo(int page) {
_currentPage = page;
rebuildUi();
}
void goBack() {
if (_currentPage == 1) {
_currentPage = 0;
rebuildUi();
} else {
_navigationService.back();
}
}
// Navigation
void pop() => _navigationService.back();
Future<void> replaceWithLogin() async =>
await _navigationService.clearStackAndShowView(const LoginView());
// Remote api calls
// Request reset code
Future<void> requestResetCode() async =>
await runBusyFuture(_requestResetCode(),
busyObject: StateObjects.requestResetCode);
Future<void> _requestResetCode() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.requestResetCode(_userData);
if (response['status'] == ResponseStatus.success) {
goTo(1);
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
// Request reset code
Future<void> resetPassword() async => await runBusyFuture(_resetPassword(),
busyObject: StateObjects.resetPassword);
Future<void> _resetPassword() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.resetPassword(_userData);
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
await replaceWithLogin();
} else {
showErrorToast(response['message']);
}
}
}
}

View File

@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/forget_password/forget_password_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/login_account.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/option_text_divider.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../forget_password_view.form.dart';
class RequestCodeScreen extends ViewModelWidget<ForgetPasswordViewModel> {
final TextEditingController emailController;
const RequestCodeScreen({
super.key,
required this.emailController,
});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 375 - half,);
}
void _inAppPop(ForgetPasswordViewModel viewModel) {
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
emailController.clear();
viewModel.resetRequestResetCodeScreen();
}
Future<void> _addUserData(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'email': emailController.text,
};
viewModel.addUserData(data);
await viewModel.requestResetCode();
}
@override
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Stack(
children: [
_buildScaffold(context: context,viewModel: viewModel),
_buildRequestResetCodeState(viewModel),
],
);
Widget _buildScaffold( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context, viewModel: viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context, viewModel: viewModel));
Widget _buildColumnScroller( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBodyWrapper(context: context, viewModel: viewModel),
);
Widget _buildBodyWrapper( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context, viewModel: viewModel),
);
Widget _buildBody( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyChildren( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButtonWrapper(viewModel)];
Widget _buildUpperColumn(ForgetPasswordViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel),
);
List<Widget> _buildUpperColumnChildren(ForgetPasswordViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
_buildSubtitle(),
verticalSpaceLarge,
_buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
verticalSpaceTiny,
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
_buildEmailValidatorWrapper(viewModel),
];
Widget _buildTitle() => Text(
'Reset password',
style: style25DG600,
);
Widget _buildSubtitle() => Text(
'Enter your email, we will send you a reset code.',
style: style14DG400,
);
Widget _buildEmailFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: emailController,
onTap: viewModel.setEmailFocus,
keyboardType: TextInputType.emailAddress,
decoration: inputDecoration(
hint: 'Email',
focus: viewModel.focusEmail,
filled: emailController.text.isNotEmpty),
);
Widget _buildEmailValidatorWrapper(ForgetPasswordViewModel viewModel) =>
viewModel.hasEmailValidationMessage
? _buildEmailValidator(viewModel)
: Container();
Widget _buildEmailValidator(ForgetPasswordViewModel viewModel) => Text(
viewModel.emailValidationMessage!,
style: style12R700,
);
Widget _buildContinueButtonWrapper(ForgetPasswordViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(ForgetPasswordViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: emailController.text.isNotEmpty &&
!viewModel.hasEmailValidationMessage
? () => _addUserData(viewModel)
: null,
backgroundColor: emailController.text.isNotEmpty &&
!viewModel.hasEmailValidationMessage
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildRequestResetCodeState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.requestResetCode)
? const PageLoadingIndicator()
: Container();
}

View File

@ -0,0 +1,306 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/custom_form_label.dart';
import '../../../widgets/custom_linear_progress_indicator.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/obscure_password.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../../../widgets/validator_list_tile.dart';
import '../forget_password_viewmodel.dart';
import '../forget_password_view.form.dart';
class ResetPasswordScreen extends ViewModelWidget<ForgetPasswordViewModel> {
final TextEditingController resetCodeController;
final TextEditingController passwordController;
final TextEditingController confirmPasswordController;
const ResetPasswordScreen(
{super.key,
required this.resetCodeController,
required this.passwordController,
required this.confirmPasswordController});
void _inAppPop(ForgetPasswordViewModel viewModel) {
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(ForgetPasswordViewModel viewModel) {
passwordController.clear();
resetCodeController.clear();
confirmPasswordController.clear();
viewModel.resetResetPasswordScreen();
}
Future<void> _reset(ForgetPasswordViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = {
'otp': resetCodeController.text,
'password': passwordController.text,
};
viewModel.addUserData(data);
await viewModel.resetPassword();
}
@override
Widget build(BuildContext context, ForgetPasswordViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(context: context, viewModel: viewModel),
);
Widget _buildScaffoldStack( {required BuildContext context,
required ForgetPasswordViewModel viewModel}) => Stack(
children: [
_buildScaffold(viewModel),
_buildResetPasswordState(viewModel),
],
);
Widget _buildScaffold( ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren( ForgetPasswordViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(ForgetPasswordViewModel viewModel) => LargeAppBar(
showBackButton: true,
showLanguageSelection: true,
onPop: () => _inAppPop(viewModel),
);
Widget _buildExpandedBody( ForgetPasswordViewModel viewModel) =>
Expanded(child: _buildColumnScroller(viewModel));
Widget _buildColumnScroller( ForgetPasswordViewModel viewModel) =>
SingleChildScrollView(
child: _buildBodyWrapper(viewModel),
);
Widget _buildBodyWrapper( ForgetPasswordViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(ForgetPasswordViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(ForgetPasswordViewModel viewModel) => [
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildFormLabel('Reset code'),
verticalSpaceSmall,
_buildResetCodeFormField(viewModel),
if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode)
verticalSpaceTiny,
if (viewModel.hasResetCodeValidationMessage && viewModel.focusResetCode)
_buildResetCodeValidationWrapper(viewModel),
verticalSpaceMedium,
_buildFormLabel('New Password'),
verticalSpaceSmall,
_buildPasswordFormField(viewModel),
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
verticalSpaceTiny,
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
_buildPasswordValidationWrapper(viewModel),
verticalSpaceMedium,
_buildFormLabel('Confirm Password'),
verticalSpaceSmall,
_buildConfirmPasswordFormField(viewModel),
if (viewModel.hasConfirmPasswordValidationMessage &&
viewModel.focusConfirmPassword)
verticalSpaceTiny,
if (viewModel.hasConfirmPasswordValidationMessage &&
viewModel.focusConfirmPassword)
_buildConfirmPasswordValidationWrapper(viewModel),
verticalSpaceMedium,
_buildLinearProgressIndicator(viewModel),
verticalSpaceSmall,
_buildCharLengthValidator(viewModel),
_buildNumberValidator(viewModel),
_buildSymbolValidator(viewModel),
_buildPasswordMatchValidator(viewModel),
verticalSpaceSmall,
_buildSignUpButton(viewModel),
verticalSpaceMedium
];
Widget _buildTitle() => Text(
'Reset password',
style: style25DG600,
);
Widget _buildFormLabel(String label) => CustomFormLabel(
label: label,
style: style14DG400,
);
Widget _buildResetCodeFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: resetCodeController,
onTap: viewModel.setResetCodeFocus,
decoration: inputDecoration(
hint: 'Reset code',
focus: viewModel.focusResetCode,
filled: passwordController.text.isNotEmpty),
);
Widget _buildResetCodeValidationWrapper(ForgetPasswordViewModel viewModel) =>
viewModel.hasResetCodeValidationMessage
? _buildResetCodeValidator(viewModel)
: Container();
Widget _buildResetCodeValidator(ForgetPasswordViewModel viewModel) => Text(
viewModel.resetCodeValidationMessage!,
style: style12R700,
);
Widget _buildPasswordFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: passwordController,
onTap: viewModel.setPasswordFocus,
obscureText: viewModel.obscurePassword,
decoration: inputDecoration(
hint: 'Password',
focus: viewModel.focusPassword,
suffix: _buildObscurePassword(viewModel),
filled: passwordController.text.isNotEmpty),
onChanged: (value) => viewModel.validatePassword(
password: passwordController.text,
confirmPassword: confirmPasswordController.text),
);
Widget _buildObscurePassword(ForgetPasswordViewModel viewModel) =>
ObscurePassword(
focus: viewModel.focusPassword,
obscure: viewModel.obscurePassword,
onTap: viewModel.setObscurePassword,
);
Widget _buildPasswordValidationWrapper(ForgetPasswordViewModel viewModel) =>
viewModel.hasPasswordValidationMessage
? _buildPasswordValidator(viewModel)
: Container();
Widget _buildPasswordValidator(ForgetPasswordViewModel viewModel) => Text(
viewModel.passwordValidationMessage!,
style: style12R700,
);
Widget _buildConfirmPasswordFormField(ForgetPasswordViewModel viewModel) =>
TextFormField(
controller: confirmPasswordController,
onTap: viewModel.setConfirmPasswordFocus,
obscureText: viewModel.obscureConfirmPassword,
onChanged: (value) => viewModel.validatePassword(
password: passwordController.text,
confirmPassword: confirmPasswordController.text),
decoration: inputDecoration(
hint: 'Confirm Password',
focus: viewModel.focusConfirmPassword,
suffix: _buildObscureConfirmPassword(viewModel),
filled: confirmPasswordController.text.isNotEmpty),
);
Widget _buildObscureConfirmPassword(ForgetPasswordViewModel viewModel) =>
ObscurePassword(
focus: viewModel.focusConfirmPassword,
obscure: viewModel.obscureConfirmPassword,
onTap: viewModel.setObscureConfirmPassword,
);
Widget _buildConfirmPasswordValidationWrapper(
ForgetPasswordViewModel viewModel) =>
viewModel.hasConfirmPasswordValidationMessage
? _buildConfirmPasswordValidator(viewModel)
: Container();
Widget _buildConfirmPasswordValidator(ForgetPasswordViewModel viewModel) =>
Text(
viewModel.confirmPasswordValidationMessage!,
style: style12R700,
);
Widget _buildLinearProgressIndicator(ForgetPasswordViewModel viewModel) =>
CustomLinearProgressIndicator(
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey,
progress: viewModel.validationProgress(),
);
Widget _buildCharLengthValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey,
label: '8 characters minimum');
Widget _buildNumberValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey,
label: 'a number');
Widget _buildSymbolValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey,
label: 'one symbol minimum');
Widget _buildPasswordMatchValidator(ForgetPasswordViewModel viewModel) =>
ValidatorListTile(
backgroundColor:
viewModel.passwordMatch ? kcPrimaryColor : kcLightGrey,
label: 'password match');
Widget _buildSignUpButton(ForgetPasswordViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Continue',
borderRadius: 12,
foregroundColor: kcWhite,
onTap: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
resetCodeController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch
? () async => await _reset(viewModel)
: null,
backgroundColor: passwordController.text.isNotEmpty &&
confirmPasswordController.text.isNotEmpty &&
resetCodeController.text.isNotEmpty &&
viewModel.number &&
viewModel.length &&
viewModel.specialChar &&
viewModel.specialChar &&
viewModel.passwordMatch
? kcPrimaryColor
: kcPrimaryColor.withOpacity(0.1),
);
Widget _buildResetPasswordState(ForgetPasswordViewModel viewModel) =>
viewModel.busy(StateObjects.resetPassword)
? const PageLoadingIndicator()
: Container();
}

View File

@ -15,8 +15,9 @@ class HomeView extends StackedView<HomeViewModel> {
HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel(); HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel();
@override @override
void onViewModelReady(HomeViewModel viewModel) { void onViewModelReady(HomeViewModel viewModel) async {
viewModel.getProfileStatus(); await viewModel.getProfileStatus();
await viewModel.getProfileData();
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -26,9 +27,7 @@ class HomeView extends StackedView<HomeViewModel> {
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy
? const StartupView( ? const StartupView(label: 'Checking user info')
label: 'Checking user info',
)
: _buildScaffold(viewModel); : _buildScaffold(viewModel);
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold( Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(

View File

@ -7,18 +7,30 @@ import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import '../../../services/api_service.dart'; import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart'; import '../../../services/authentication_service.dart';
import '../../../services/image_downloader_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
class HomeViewModel extends BaseViewModel { class HomeViewModel extends ReactiveViewModel {
final _apiService = locator<ApiService>(); final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>(); final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>(); final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _bottomSheetService = locator<BottomSheetService>(); final _bottomSheetService = locator<BottomSheetService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
final _imageDownloaderService = locator<ImageDownloaderService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// Bottom navigation // Bottom navigation
int _currentIndex = 0; int _currentIndex = 0;
@ -46,33 +58,77 @@ class HomeViewModel extends BaseViewModel {
); );
} }
Future<void> saveProfileStatus(bool value) async =>
await _authenticationService.saveProfileStatus(value);
// Navigation // Navigation
Future<void> replaceWithFailure() async =>
await _navigationService.clearStackAndShowView(
const FailureView(label: 'Check your internet connection to proceed'),
);
Future<void> replaceWithOnboarding() async => Future<void> replaceWithOnboarding() async =>
await _navigationService.replaceWithOnboardingView(); await _navigationService.replaceWithOnboardingView();
// Remote api calls // Remote api calls
Future<void> getProfileStatus() async {
Map<String, dynamic> response = // Profile data
await runBusyFuture<Map<String, dynamic>>(_getProfileStatus()); Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding(); Future<void> _getProfileData() async {
print('RESPONSE FOR USER DATA ${_user?.firstName}');
if (!(_user?.userInfoLoaded ?? false)) {
print('RESPONSE FOR USER DATA 1');
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = {};
if (_user?.profileCompleted != null &&
(_user?.profileCompleted ?? false)) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
String image =
await _imageDownloaderService.downloader(user.profilePicture);
await _authenticationService.saveUserData(
image: image, data: user);
}
}
}
}
} }
} }
Future<Map<String, dynamic>> _getProfileStatus() async { // Profile status
Map<String, dynamic> response = {}; Future<void> getProfileStatus() async =>
UserModel user = await _authenticationService.getUser(); await runBusyFuture(_getProfileStatus());
if (user.profileCompleted == null) { Future<void> _getProfileStatus() async {
if (await _statusChecker.checkConnection()) { if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileStatus(user); Map<String, dynamic> response = {};
} else {
if (_user?.profileCompleted == null) {
if (await _statusChecker.checkConnection()) {
response = await _apiService.getProfileStatus(_user);
} else {
await replaceWithFailure();
}
} else if (!(_user?.profileCompleted ?? false)) {
response = {'data': false, 'status': ResponseStatus.success}; response = {'data': false, 'status': ResponseStatus.success};
} else {
response = {'data': true, 'status': ResponseStatus.success};
} }
} else {
response = {'data': true, 'status': ResponseStatus.success};
}
return response; if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success &&
response['data']) {
await saveProfileStatus(response['data']);
}
}
} }
} }

View File

@ -61,7 +61,7 @@ class LanguageView extends StackedView<LanguageViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildLanguages(viewModel) _buildLanguages(viewModel)
]; ];
@ -72,22 +72,18 @@ class LanguageView extends StackedView<LanguageViewModel> {
); );
Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar( Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar(
title: 'Language Preference',
onTap: viewModel.pop, onTap: viewModel.pop,
title: 'Language Preference',
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Choose your language', 'Choose your language',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'You can switch languages anytime', 'You can switch languages anytime',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder( Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder(

View File

@ -38,12 +38,15 @@ class LearnView extends StackedView<LearnViewModel> {
Widget _buildColumn(LearnViewModel viewModel) => Column( Widget _buildColumn(LearnViewModel viewModel) => Column(
children: [ children: [
verticalSpaceMedium, verticalSpaceMedium,
_buildAppBar(), _buildAppBar(viewModel),
_buildLevelsColumnWrapper(viewModel) _buildLevelsColumnWrapper(viewModel)
], ],
); );
Widget _buildAppBar() => const LearnAppBar(); Widget _buildAppBar(LearnViewModel viewModel) => LearnAppBar(
name: viewModel.user?.firstName,
profileImage: viewModel.user?.profilePicture,
);
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) => Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel)); Expanded(child: _buildLevelsColumnScrollView(viewModel));

View File

@ -1,12 +1,22 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
class LearnViewModel extends BaseViewModel { class LearnViewModel extends ReactiveViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get user => _authenticationService.user;
final List<Map<String, dynamic>> _learnLevels = [ final List<Map<String, dynamic>> _learnLevels = [
{ {

View File

@ -4,7 +4,6 @@ import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/widgets/learn_lesson_tile.dart'; import 'package:yimaru_app/ui/widgets/learn_lesson_tile.dart';
import 'package:yimaru_app/ui/widgets/module_progress.dart'; import 'package:yimaru_app/ui/widgets/module_progress.dart';
import 'package:yimaru_app/ui/widgets/motivation_card.dart'; import 'package:yimaru_app/ui/widgets/motivation_card.dart';
import 'package:yimaru_app/ui/widgets/overall_module_progress.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
@ -118,19 +117,24 @@ class LearnLessonView extends StackedView<LearnLessonViewModel> {
itemCount: viewModel.lessons.length, itemCount: viewModel.lessons.length,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => _buildTile( itemBuilder: (context, index) => _buildTile(
title: viewModel.lessons[index]['title'], title: viewModel.lessons[index]['title'],
status: viewModel.lessons[index]['status'], status: viewModel.lessons[index]['status'],
thumbnail: viewModel.lessons[index]['thumbnail']), thumbnail: viewModel.lessons[index]['thumbnail'],
onLessonTap: () async =>
await viewModel.navigateToLearnLessonDetail(),
),
); );
Widget _buildTile({ Widget _buildTile({
required String title, required String title,
required String thumbnail, required String thumbnail,
GestureTapCallback? onLessonTap,
required ProgressStatuses status, required ProgressStatuses status,
}) => }) =>
LearnLessonTile( LearnLessonTile(
title: title, title: title,
status: status, status: status,
thumbnail: thumbnail, thumbnail: thumbnail,
onLessonTap: onLessonTap,
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/enmus.dart'; import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
@ -8,7 +9,6 @@ class LearnLessonViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
// Lessons // Lessons
// Downloads
final List<Map<String, dynamic>> _lessons = [ final List<Map<String, dynamic>> _lessons = [
{ {
'title': '1.1 Introducing Yourself', 'title': '1.1 Introducing Yourself',
@ -31,4 +31,7 @@ class LearnLessonViewModel extends BaseViewModel {
// Navigation // Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnLessonDetail() async =>
await _navigationService.navigateToLearnLessonDetailView();
} }

View File

@ -0,0 +1,178 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/empty_video_player.dart';
import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_lesson_detail_viewmodel.dart';
class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
const LearnLessonDetailView({Key? key}) : super(key: key);
Future<void> _navigate(LearnLessonDetailViewModel viewModel)async{
await viewModel.pause();
await viewModel.navigateToLearnPractice();
}
// @override
// void onDispose(LearnLessonDetailViewModel viewModel) {
// print('DISPOSED');
// viewModel.dispose();
// super.onDispose(viewModel);
// }
@override
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
await viewModel.initializePlayer();
super.onViewModelReady(viewModel);
}
@override
LearnLessonDetailViewModel viewModelBuilder(BuildContext context) =>
LearnLessonDetailViewModel();
@override
Widget builder(
BuildContext context,
LearnLessonDetailViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnLessonDetailViewModel viewModel) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnLessonDetailViewModel viewModel) =>
SafeArea(child: _buildColumn(viewModel));
Widget _buildColumn(LearnLessonDetailViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBarWrapper(viewModel),
_buildBodyColumnWrapper(viewModel),
],
);
Widget _buildAppBarWrapper(LearnLessonDetailViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildAppBar(viewModel));
Widget _buildAppBar(LearnLessonDetailViewModel viewModel) => SmallAppBar(
onTap: viewModel.pop,
);
Widget _buildBodyColumnWrapper(LearnLessonDetailViewModel viewModel) =>
Expanded(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnLessonDetailViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnLessonDetailViewModel viewModel) =>
[
_buildLevelsColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildLevelsColumnWrapper(LearnLessonDetailViewModel viewModel) =>
Expanded(child: _buildLevelsColumnScrollView(viewModel));
Widget _buildLevelsColumnScrollView(LearnLessonDetailViewModel viewModel) =>
SingleChildScrollView(
child: _buildLevelsColumn(viewModel),
);
Widget _buildLevelsColumn(LearnLessonDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildLevelsColumnChildren(viewModel),
);
List<Widget> _buildLevelsColumnChildren(
LearnLessonDetailViewModel viewModel) =>
[
verticalSpaceMedium,
_buildTitleWrapper(),
verticalSpaceLarge,
_buildVideoPlayerWrapper(viewModel),
verticalSpaceMedium,
_buildDescriptionWrapper(),
];
Widget _buildTitleWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildTitle(),
);
Widget _buildTitle() => Text(
'1.3 Common Greetings',
style: style16DG600,
);
Widget _buildVideoPlayerWrapper(LearnLessonDetailViewModel viewModel) =>
Container(
height: 200,
color: kcBlack,
width: double.maxFinite,
child: _buildVideoPlayerState(viewModel),
);
Widget _buildVideoPlayerState(LearnLessonDetailViewModel viewModel) =>
viewModel.chewieController != null &&
viewModel.videoPlayerController != null &&
!viewModel.busy(StateObjects.loadLessonVideo)
? _buildVideoPlayer(viewModel)
: _buildEmptyVideoPlayer();
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
_buildChewiePlayer(viewModel);
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
Chewie(controller: viewModel.chewieController!);
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
Widget _buildDescriptionWrapper() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildDescription(),
);
Widget _buildDescription() => Text(
'In this lesson, youll explore how to start simple conversations by greeting others in polite and friendly ways. Youll practice different greetings for morning, afternoon, and evening, as well as casual and formal situations. By the end, youll know how to confidently say hello, ask how someone is, and respond naturally.',
style: style14DG600,
);
Widget _buildContinueButtonWrapper(LearnLessonDetailViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(
left: 15,
right: 15,
bottom: 50,
),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnLessonDetailViewModel viewModel) =>
CustomElevatedButton(
height: 55,
text: 'Practice',
borderRadius: 12,
foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor,
onTap: ()async => await _navigate(viewModel),
);
}

View File

@ -0,0 +1,72 @@
import 'package:chewie/chewie.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:video_player/video_player.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/app_constants.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import '../../../app/app.locator.dart';
import '../../../services/status_checker_service.dart';
class LearnLessonDetailViewModel extends BaseViewModel {
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>();
// Video player config
ChewieController? _chewieController;
ChewieController? get chewieController => _chewieController;
VideoPlayerController? _videoPlayerController;
VideoPlayerController? get videoPlayerController => _videoPlayerController;
// Video player
Future<void> initializePlayer() async =>
await runBusyFuture(_initializePlayer(),
busyObject: StateObjects.loadLessonVideo);
Future<void> _initializePlayer() async {
_videoPlayerController =
VideoPlayerController.networkUrl(Uri.parse(kSampleVideoUrl));
await _videoPlayerController?.initialize();
if (_videoPlayerController != null) {
print('Initialized');
_chewieController = ChewieController(
looping: true,
autoPlay: true,
showOptions: true,
showControls: true,
aspectRatio: 16 / 9,
autoInitialize: true,
allowedScreenSleep: false,
videoPlayerController: _videoPlayerController!,
materialProgressColors: buildChewieProgressIndicator);
}
// rebuildUi();
}
Future<void> pause()async{
await _chewieController?.pause();
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToLearnPractice() async=>await _navigationService.navigateToLearnPracticeView();
}

View File

@ -73,20 +73,14 @@ class LearnModuleView extends StackedView<LearnModuleViewModel> {
_buildListView(viewModel) _buildListView(viewModel)
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'A1 - Beginner', 'A1 - Beginner',
style: TextStyle( style: style18P600,
fontSize: 18,
color: kcPrimaryColor,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubTitle() => Text(
'Your Current Level', 'Your Current Level',
style: TextStyle( style: style14DG400,
color: kcDarkGrey,
),
); );
Widget _buildOverallProgress() => const OverallLearnProgress(); Widget _buildOverallProgress() => const OverallLearnProgress();

View File

@ -37,5 +37,6 @@ class LearnModuleViewModel extends BaseViewModel {
void pop() => _navigationService.back(); void pop() => _navigationService.back();
Future<void> navigateToLearnLesson() async=> await _navigationService.navigateToLearnLessonView(); Future<void> navigateToLearnLesson() async =>
await _navigationService.navigateToLearnLessonView();
} }

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/listen_speaker_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/practice_intro_screen.dart';
import 'package:yimaru_app/ui/views/learn_practice/screens/start_practice_screen.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/speaking_partner_image.dart';
import '../../common/app_colors.dart';
import '../../common/ui_helpers.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/small_app_bar.dart';
import 'learn_practice_viewmodel.dart';
class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
const LearnPracticeView({Key? key}) : super(key: key);
@override
LearnPracticeViewModel viewModelBuilder(BuildContext context) =>
LearnPracticeViewModel();
@override
Widget builder(
BuildContext context,
LearnPracticeViewModel viewModel,
Widget? child,
) =>
_buildPracticeScreensWrapper(viewModel);
Widget _buildPracticeScreensWrapper(LearnPracticeViewModel viewModel) => PopScope(
canPop: true,
onPopInvokedWithResult: (value, data) {
if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
},
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LearnPracticeViewModel viewModel) => Stack(children: [
_buildBody(viewModel),
//_buildLoginWithEmailState(viewModel),
//_buildLoginWithGoogleState(viewModel)
]);
Widget _buildBody(LearnPracticeViewModel viewModel) =>
IndexedStack(
index: viewModel.currentIndex, children: _buildScreens());
List<Widget> _buildScreens() => [
_buildPracticeIntroScreen(),
_buildStartPracticeScreen(),
_buildListenSpeakerScreen()
];
Widget _buildPracticeIntroScreen() => const PracticeIntroScreen();
Widget _buildStartPracticeScreen() => const StartPracticeScreen();
Widget _buildListenSpeakerScreen() => const ListenSpeakerScreen();
}

View File

@ -0,0 +1,34 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart';
class LearnPracticeViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>();
// In-app navigation
int _currentIndex = 0;
int get currentIndex => _currentIndex;
// In-app navigation
void goTo(int page) {
_currentIndex = page;
rebuildUi();
}
void goBack() {
if(_currentIndex == 0){
pop();
}else{
_currentIndex--;
rebuildUi();
}
}
// Navigation
void pop() => _navigationService.back();
}

View File

@ -0,0 +1,254 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/cancel_learn_practice_sheet.dart';
import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_column_button.dart';
import '../../../widgets/small_app_bar.dart';
class ListenSpeakerScreen extends ViewModelWidget<LearnPracticeViewModel> {
const ListenSpeakerScreen({super.key});
Future<void> _showSheet(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) async =>
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: kcTransparent,
builder: (_) => _buildSheet(viewModel),
);
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
SafeArea(
child:
_buildBodyColumnWrapper(context: context, viewModel: viewModel));
Widget _buildBodyColumnWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyStack(context: context, viewModel: viewModel),
);
Widget _buildBodyStack(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Stack(
children: [
_buildBodyColumn(context: context, viewModel: viewModel),
_buildProgressIndicatorWrapper()
],
);
Widget _buildBodyColumn(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildBodyColumnChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildBodyColumnChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildAppBarWrapper(viewModel),
_buildSpeakingIndicatorWrapper(viewModel),
_buildLowerButtonsSectionWrapper(context: context, viewModel: viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildSpeakingIndicatorWrapper(LearnPracticeViewModel viewModel) =>
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: _buildSpeakingIndicatorChildren(),
);
List<Widget> _buildSpeakingIndicatorChildren() =>
[_buildSpeakerLabel(), verticalSpaceMedium, _buildSpeakingIndicator()];
Widget _buildSpeakerLabel() => Text(
'Daniel is speaking...',
style: style14P400,
textAlign: TextAlign.center,
);
Widget _buildSpeakingIndicator() => Container(
height: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
radius: 0.7,
stops: const [
0.2,
0.25,
0.45,
0.75,
1,
],
center: Alignment.center,
colors: [
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.15),
kcPrimaryColor.withOpacity(0.1),
kcPrimaryColor.withOpacity(0.05),
],
// quarterly spread
),
),
child: _buildSpinner(),
);
Widget _buildSpinner() => const SpinKitWave(
size: 20,
color: kcPrimaryColor,
type: SpinKitWaveType.center,
);
Widget _buildLowerButtonsSectionWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child:
_buildLowerButtonsSection(context: context, viewModel: viewModel),
);
Widget _buildLowerButtonsSection(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(context: context, viewModel: viewModel),
verticalSpaceMedium,
];
Widget _buildActionLabel() => Text(
'Tap the microphone to speak',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children:
_buildButtonsRowChildren(context: context, viewModel: viewModel),
);
List<Widget> _buildButtonsRowChildren(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
[
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildCancelButtonWrapper(context: context, viewModel: viewModel)
];
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildMicIcon(),
);
Widget _buildMicIcon() => const Icon(
Icons.mic,
size: 35,
color: kcWhite,
);
Widget _buildCancelButtonWrapper(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
Expanded(
child: _buildCancelButton(context: context, viewModel: viewModel));
Widget _buildCancelButton(
{required BuildContext context,
required LearnPracticeViewModel viewModel}) =>
CustomColumnButton(
color: kcRed,
label: 'Cancel',
icon: Icons.close,
onTap: () async =>
await _showSheet(context: context, viewModel: viewModel),
);
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
CancelLearnPracticeSheet(
onTap: viewModel.pop,
);
Widget _buildProgressIndicatorWrapper() => Positioned(
top: 75,
left: 0,
right: 0,
child: _buildProgressIndicator(),
);
Widget _buildProgressIndicator() => const CustomLinearProgressIndicator(
progress: 0.7,
activeColor: kcPrimaryColor,
backgroundColor: kcVeryLightGrey);
}

View File

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_view.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/small_app_bar.dart';
import '../../../widgets/speaking_partner_image.dart';
class PracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
const PracticeIntroScreen({super.key});
@override
Widget build(BuildContext context,LearnPracticeViewModel viewModel) => _buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildColumnWrapper(viewModel));
Widget _buildColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
);
Widget _buildColumn(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
_buildBodyColumnWrapper(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded(
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildPracticeColumnWrapper(viewModel),
_buildContinueButtonWrapper(viewModel)
];
Widget _buildPracticeColumnWrapper(LearnPracticeViewModel viewModel) =>
Expanded(child: _buildPracticeColumnScrollView(viewModel));
Widget _buildPracticeColumnScrollView(LearnPracticeViewModel viewModel) =>
SingleChildScrollView(
child: _buildPracticeColumn(viewModel),
);
Widget _buildPracticeColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildPracticeColumnChildren(viewModel),
);
List<Widget> _buildPracticeColumnChildren(LearnPracticeViewModel viewModel) =>
[
verticalSpaceMassive,
_buildImage(),
verticalSpaceMedium,
_buildPartnerName(),
verticalSpaceMedium,
_buildTitle(),
verticalSpaceMedium,
_buildSubtitle()
];
Widget _buildImage() => const SpeakingPartnerImage(radius: 75,);
Widget _buildPartnerName() => Text.rich(
TextSpan(text: 'Daniel', style: style14DG600, children: [
TextSpan(
text: ' - Your Speaking Partner',
style: style14MG400,
)
]),
);
Widget _buildTitle() => Text(
'Let \'s practice what you just learnt!',
style: style25DG600,
textAlign: TextAlign.center,
);
Widget _buildSubtitle() => Text(
'Ill ask you a few questions, and you can respond naturally.',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildContinueButtonWrapper(LearnPracticeViewModel viewModel) =>
Padding(
padding: const EdgeInsets.only(bottom: 50),
child: _buildContinueButton(viewModel),
);
Widget _buildContinueButton(LearnPracticeViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
text: 'Start Practice',
foregroundColor: kcWhite,
onTap: ()=> viewModel.goTo(1),
backgroundColor: kcPrimaryColor,
);
}

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/views/learn_practice/learn_practice_viewmodel.dart';
import 'package:yimaru_app/ui/widgets/custom_column_button.dart';
import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart';
import '../../../widgets/small_app_bar.dart';
class StartPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
const StartPracticeScreen({super.key});
@override
Widget build(BuildContext context, LearnPracticeViewModel viewModel) =>
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(LearnPracticeViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
);
Widget _buildScaffold(LearnPracticeViewModel viewModel) =>
SafeArea(child: _buildBodyColumnWrapper(viewModel));
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBodyColumn(viewModel),
);
Widget _buildBodyColumn(LearnPracticeViewModel viewModel) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyColumnChildren(viewModel),
);
List<Widget> _buildBodyColumnChildren(LearnPracticeViewModel viewModel) => [
_buildAppBarWrapper(viewModel),
_buildStartButtonWrapper(viewModel),
_buildLowerButtonsSectionWrapper(viewModel)
];
Widget _buildAppBarWrapper(LearnPracticeViewModel viewModel) => Column(
children: [
verticalSpaceMedium,
_buildAppBar(viewModel),
],
);
Widget _buildAppBar(LearnPracticeViewModel viewModel) => SmallAppBar(
onTap: viewModel.goBack,
title: 'Practice Speaking',
);
Widget _buildStartButtonWrapper(LearnPracticeViewModel viewModel) => Expanded(
child: _buildStartButtonContainer(viewModel),
);
Widget _buildStartButtonContainer(LearnPracticeViewModel viewModel) =>
GestureDetector(
onTap: () => viewModel.goTo(2),
child: _buildStartButton(),
);
Widget _buildStartButton() => Container(
width: 150,
height: 150,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: SweepGradient(
stops: const [
0.0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.8,
0.9,
1,
],
endAngle: 8,
startAngle: 0.0,
center: Alignment.center,
colors: [
kcPrimaryColor.withOpacity(0.3),
kcIndigo.withOpacity(0.2),
kcIndigo.withOpacity(0.3),
kcIndigo.withOpacity(0.4),
kcIndigo.withOpacity(0.5),
kcPrimaryColor.withOpacity(0.5),
kcPrimaryColor.withOpacity(0.4),
kcPrimaryColor.withOpacity(0.3),
kcPrimaryColor.withOpacity(0.2),
kcPrimaryColor.withOpacity(0.5),
],
// quarterly spread
),
),
child: _buildStartText(),
);
Widget _buildStartText() => Text(
'Start',
style: style25W600,
);
Widget _buildLowerButtonsSectionWrapper(LearnPracticeViewModel viewMode) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: _buildLowerButtonsSection(viewMode),
);
Widget _buildLowerButtonsSection(LearnPracticeViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildLowerButtonsSectionChildren(viewModel),
);
List<Widget> _buildLowerButtonsSectionChildren(
LearnPracticeViewModel viewModel) =>
[
_buildActionLabel(),
verticalSpaceMedium,
_buildButtonsRowWrapper(),
verticalSpaceMedium,
];
Widget _buildActionLabel() => Text(
'Tap the microphone to speak',
style: style14DG400,
textAlign: TextAlign.center,
);
Widget _buildButtonsRowWrapper() => Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildButtonsRowChildren(),
);
List<Widget> _buildButtonsRowChildren() => [
_buildReplyButtonWrapper(),
_buildMicButtonWrapper(),
_buildEmptySpace()
];
Widget _buildReplyButtonWrapper() => Expanded(child: _buildReplyButton());
Widget _buildReplyButton() => const CustomColumnButton(
icon: Icons.replay, label: 'Reply', color: kcPrimaryColor);
Widget _buildMicButtonWrapper() => Expanded(child: _buildMicButton());
Widget _buildMicButton() => ElevatedButton(
onPressed: () {},
style: const ButtonStyle(
shape: WidgetStatePropertyAll(CircleBorder()),
padding: WidgetStatePropertyAll(EdgeInsets.all(15)),
shadowColor: WidgetStatePropertyAll(kcPrimaryColor),
backgroundColor: WidgetStatePropertyAll(kcPrimaryColor),
),
child: _buildMicIcon(),
);
Widget _buildMicIcon() => const Icon(
Icons.mic,
size: 35,
color: kcWhite,
);
Widget _buildEmptySpace() => Expanded(child: Container());
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart'; import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart'; import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart';
import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart'; import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart';
import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart'; import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart';
@ -24,10 +25,18 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
@override @override
void onViewModelReady(LoginViewModel viewModel) { void onViewModelReady(LoginViewModel viewModel) {
_clearData();
syncFormWithViewModel(viewModel); syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
void _clearData() {
otpController.clear();
emailController.clear();
passwordController.clear();
phoneNumberController.clear();
}
@override @override
LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel(); LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel();
@ -45,35 +54,11 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
if (!value) return; if (!value) return;
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
}, },
child: _buildScaffoldWrapper(viewModel)); child: _buildBody(viewModel));
Widget _buildScaffoldWrapper(LoginViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(LoginViewModel viewModel) =>
Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]);
Widget _buildScaffold(LoginViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(LoginViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody(LoginViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(LoginViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(LoginViewModel viewModel) => Widget _buildBody(LoginViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); IndexedStack(index: viewModel.currentIndex, children: _buildScreens());
@ -94,6 +79,4 @@ class LoginView extends StackedView<LoginViewModel> with $LoginView {
otpController: otpController, otpController: otpController,
phoneNumberController: phoneNumberController); phoneNumberController: phoneNumberController);
Widget _buildBusyLogin(LoginViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.locator.dart'; import 'package:yimaru_app/app/app.locator.dart';
@ -7,16 +8,25 @@ import 'package:yimaru_app/models/user_model.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/image_downloader_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart'; import '../../common/enmus.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../home/home_view.dart'; import '../home/home_view.dart';
class LoginViewModel extends FormViewModel { class LoginViewModel extends FormViewModel {
final _apiService = locator<ApiService>(); final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _googleAuthService = locator<GoogleAuthService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
// Navigation // In-app navigation
int _currentIndex = 0; int _currentIndex = 0;
int get currentIndex => _currentIndex; int get currentIndex => _currentIndex;
@ -106,36 +116,7 @@ class LoginViewModel extends FormViewModel {
_userData.clear(); _userData.clear();
} }
// Remote api calls // In app navigation
Future<void> login() async {
Map<String, dynamic> response =
await runBusyFuture<Map<String, dynamic>>(_login());
if (response['status'] == ResponseStatus.success) {
await replaceWithHome();
}
}
Future<Map<String, dynamic>> _login() async {
Map<String, dynamic> response = await _apiService.login(_userData);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserData(data);
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
return response;
}
// Navigation
void goTo(int page) { void goTo(int page) {
_currentIndex = page; _currentIndex = page;
rebuildUi(); rebuildUi();
@ -153,9 +134,70 @@ class LoginViewModel extends FormViewModel {
} }
} }
// Navigation
Future<void> navigateToRegister() async => Future<void> navigateToRegister() async =>
await _navigationService.navigateToRegisterView(); await _navigationService.navigateToRegisterView();
Future<void> navigateToForgetPassword() async =>
await _navigationService.navigateToForgetPasswordView();
Future<void> replaceWithHome() async => Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView()); await _navigationService.clearStackAndShowView(const HomeView());
// Remote api calls
// Login with email
Future<void> emailLogin() async => await runBusyFuture(_emailLogin(),
busyObject: StateObjects.loginWithEmail);
Future<void> _emailLogin() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response = await _apiService.emailLogin(_userData);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserCredential(data);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
Future<void> signInWithGoogle() async => await runBusyFuture(_signInWithGoogle(),
busyObject: StateObjects.loginWithGoogle);
Future<void> _signInWithGoogle() async {
if (await _statusChecker.checkConnection()) {
GoogleSignInAccount? googleUser = await _googleAuthService.googleAuth();
Map<String, dynamic> data = {
'id_token': googleUser?.authentication.idToken ?? '',
};
Map<String, dynamic> response = await _apiService.googleAuth(data);
if (response['status'] == ResponseStatus.success) {
UserModel user = response['data'] as UserModel;
Map<String, dynamic> data = {
'userId': user.userId,
'accessToken': user.accessToken,
'refreshToken': user.refreshToken
};
await _authenticationService.saveUserCredential(data);
clearUserData();
await replaceWithHome();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
} }

View File

@ -8,6 +8,7 @@ import 'package:yimaru_app/ui/widgets/custom_cursor.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../login_viewmodel.dart'; import '../login_viewmodel.dart';
import '../login_view.form.dart'; import '../login_view.form.dart';
@ -20,29 +21,68 @@ class LoginOtpScreen extends ViewModelWidget<LoginViewModel> {
required this.otpController, required this.otpController,
required this.phoneNumberController}); required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 325 - half,);
}
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context,viewModel: viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column( Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
crossAxisAlignment: CrossAxisAlignment.start, backgroundColor: kcBackgroundColor,
mainAxisAlignment: MainAxisAlignment.spaceBetween, body: _buildScaffold(context: context,viewModel: viewModel),
children: _buildBodyChildren(viewModel), );
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildContinueButton(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel), children: _buildUpperColumnChildren(viewModel),
); );
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,

View File

@ -4,9 +4,12 @@ import 'package:yimaru_app/ui/views/login/login_view.form.dart';
import 'package:yimaru_app/ui/widgets/obscure_password.dart'; import 'package:yimaru_app/ui/widgets/obscure_password.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/enmus.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/option_text_divider.dart'; import '../../../widgets/option_text_divider.dart';
import '../../../widgets/page_loading_indicator.dart';
import '../../../widgets/register_for_account.dart'; import '../../../widgets/register_for_account.dart';
import '../login_viewmodel.dart'; import '../login_viewmodel.dart';
@ -19,6 +22,12 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
required this.emailController, required this.emailController,
required this.passwordController}); required this.passwordController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 25 - half,);
}
Future<void> _login(LoginViewModel viewModel) async { Future<void> _login(LoginViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -28,27 +37,63 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
}; };
viewModel.addUserData(data); viewModel.addUserData(data);
await viewModel.login(); await viewModel.emailLogin();
} }
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel); _buildScaffoldWrapper(context: context,viewModel: viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column( Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
crossAxisAlignment: CrossAxisAlignment.start, backgroundColor: kcBackgroundColor,
mainAxisAlignment: MainAxisAlignment.spaceBetween, body: _buildScaffoldStack(context: context,viewModel: viewModel),
children: _buildBodyChildren(viewModel), );
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) => Widget _buildScaffoldStack({required BuildContext context,required LoginViewModel viewModel}) => Stack(children: [
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; _buildScaffold(context: context,viewModel: viewModel),
_buildLoginWithEmailState(viewModel),
_buildLoginWithGoogleState(viewModel)
]);
Widget _buildColumnScroller(LoginViewModel viewModel) => Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -58,7 +103,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubTitleWrapper(viewModel), _buildSubtitleWrapper(viewModel),
verticalSpaceLarge, verticalSpaceLarge,
_buildEmailFormField(viewModel), _buildEmailFormField(viewModel),
if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) if (viewModel.hasEmailValidationMessage && viewModel.focusEmail)
@ -71,19 +116,15 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
verticalSpaceTiny, verticalSpaceTiny,
if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword)
_buildPasswordValidationWrapper(viewModel), _buildPasswordValidationWrapper(viewModel),
_buildForgetPasswordTextButtonWrapper(), _buildForgetPasswordTextButtonWrapper(viewModel),
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Welcome Back', 'Welcome Back',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(), onTap: () async => await viewModel.navigateToRegister(),
); );
@ -104,11 +145,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildEmailValidator(LoginViewModel viewModel) => Text( Widget _buildEmailValidator(LoginViewModel viewModel) => Text(
viewModel.emailValidationMessage!, viewModel.emailValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField( Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField(
@ -135,26 +172,23 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
Widget _buildPasswordValidator(LoginViewModel viewModel) => Text( Widget _buildPasswordValidator(LoginViewModel viewModel) => Text(
viewModel.passwordValidationMessage!, viewModel.passwordValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildForgetPasswordTextButtonWrapper() => Align( Widget _buildForgetPasswordTextButtonWrapper(LoginViewModel viewModel) =>
Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: _buildForgetPasswordTextButton(), child: _buildForgetPasswordTextButton(viewModel),
); );
Widget _buildForgetPasswordTextButton() => TextButton( Widget _buildForgetPasswordTextButton(LoginViewModel viewModel) => TextButton(
onPressed: () {}, onPressed: () async => await viewModel.navigateToForgetPassword(),
child: _buildForgetPasswordText(), child: _buildForgetPasswordText(),
); );
Widget _buildForgetPasswordText() => const Text( Widget _buildForgetPasswordText() => Text(
'Forget Password?', 'Forget Password?',
style: TextStyle(color: kcPrimaryColor), style: style14P400,
); );
Widget _buildLowerColumn(LoginViewModel viewModel) => Column( Widget _buildLowerColumn(LoginViewModel viewModel) => Column(
@ -164,13 +198,15 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(LoginViewModel viewModel) => [
_buildContinueButton(viewModel), _buildContinueButton(viewModel),
_buildLoginWithGoogleButton(viewModel),
_buildOptionTextDivider(), _buildOptionTextDivider(),
_buildLoginWithEmailButton(viewModel), _buildLoginWithPhoneButton(viewModel),
verticalSpaceMedium verticalSpaceMedium
]; ];
Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton(
height: 55, height: 55,
safe: false,
text: 'Continue', text: 'Continue',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcWhite, foregroundColor: kcWhite,
@ -184,17 +220,40 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
: kcPrimaryColor.withOpacity(0.1), : kcPrimaryColor.withOpacity(0.1),
); );
Widget _buildLoginWithGoogleButton(LoginViewModel viewModel) =>
CustomElevatedButton(
height: 55,
borderRadius: 12,
backgroundColor: kcWhite,
text: 'Login with Google',
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor,
leadingImage: 'assets/icons/google.png',
onTap: () async => await viewModel.signInWithGoogle(),
);
Widget _buildOptionTextDivider() => const OptionTextDivider(); Widget _buildOptionTextDivider() => const OptionTextDivider();
Widget _buildLoginWithEmailButton(LoginViewModel viewModel) => Widget _buildLoginWithPhoneButton(LoginViewModel viewModel) =>
CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
backgroundColor: kcWhite, backgroundColor: kcWhite,
leadingIcon: Icons.phone, leadingIcon: Icons.phone,
borderColor: kcPrimaryColor, borderColor: kcPrimaryColor,
onTap: () => viewModel.goTo(1),
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
text: 'Login with Phone Number', text: 'Login with Phone Number',
onTap: () => viewModel.goTo(1),
); );
Widget _buildLoginWithEmailState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithEmail)
? const PageLoadingIndicator()
: Container();
Widget _buildLoginWithGoogleState(LoginViewModel viewModel) =>
viewModel.busy(StateObjects.loginWithGoogle)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -8,6 +8,7 @@ import 'package:yimaru_app/ui/widgets/register_for_account.dart';
import '../../../common/app_colors.dart'; import '../../../common/app_colors.dart';
import '../../../common/ui_helpers.dart'; import '../../../common/ui_helpers.dart';
import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_elevated_button.dart';
import '../../../widgets/large_app_bar.dart';
import '../../../widgets/phone_number_prefix.dart'; import '../../../widgets/phone_number_prefix.dart';
import '../login_view.form.dart'; import '../login_view.form.dart';
@ -17,34 +18,75 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
const LoginWithPhoneNumberScreen( const LoginWithPhoneNumberScreen(
{super.key, required this.phoneNumberController}); {super.key, required this.phoneNumberController});
Widget getPadding(context){
double half = screenHeight(context)/2;
return SizedBox(height: half + 175 - half,);
}
@override @override
Widget build(BuildContext context, LoginViewModel viewModel) => Widget build(BuildContext context, LoginViewModel viewModel) =>
_buildBody(viewModel);
Widget _buildBody(LoginViewModel viewModel) => Column( _buildScaffoldWrapper(context: context,viewModel: viewModel);
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: _buildBodyChildren(viewModel),
);
List<Widget> _buildBodyChildren(LoginViewModel viewModel) =>
[_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)];
Widget _buildColumnScroller(LoginViewModel viewModel) =>
Widget _buildScaffoldWrapper({required BuildContext context,required LoginViewModel viewModel}) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(context: context,viewModel: viewModel),
);
Widget _buildScaffold({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildScaffoldChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildAppBar(viewModel), _buildExpandedBody(context: context,viewModel: viewModel)];
Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar(
showBackButton: false,
showLanguageSelection: true,
);
Widget _buildExpandedBody({required BuildContext context,required LoginViewModel viewModel}) =>
Expanded(child: _buildColumnScroller(context: context,viewModel: viewModel));
Widget _buildColumnScroller({required BuildContext context,required LoginViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildUpperColumn(viewModel), child: _buildBodyWrapper(context: context,viewModel: viewModel),
); );
Widget _buildBodyWrapper({required BuildContext context,required LoginViewModel viewModel}) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(context: context,viewModel: viewModel),
);
Widget _buildBody({required BuildContext context,required LoginViewModel viewModel}) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(context: context,viewModel: viewModel),
);
List<Widget> _buildBodyChildren({required BuildContext context,required LoginViewModel viewModel}) =>
[_buildUpperColumn(viewModel),getPadding(context), _buildLowerColumn(viewModel)];
Widget _buildUpperColumn(LoginViewModel viewModel) => Column( Widget _buildUpperColumn(LoginViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildUpperColumnChildren(viewModel), children: _buildUpperColumnChildren(viewModel),
); );
List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [ List<Widget> _buildUpperColumnChildren(LoginViewModel viewModel) => [
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
_buildSubTitleWrapper(viewModel), _buildSubtitleWrapper(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildSubtitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
@ -57,22 +99,18 @@ class LoginWithPhoneNumberScreen extends ViewModelWidget<LoginViewModel> {
_buildPhoneNumberValidatorWrapper(viewModel), _buildPhoneNumberValidatorWrapper(viewModel),
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Welcome Back', 'Welcome Back',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( Widget _buildSubtitleWrapper(LoginViewModel viewModel) => RegisterForAccount(
onTap: () async => await viewModel.navigateToRegister(), onTap: () async => await viewModel.navigateToRegister(),
); );
Widget _buildSubtitle() => const Text( Widget _buildSubtitle() => Text(
'Enter your phone number. We will send you a confirmation code there', 'Enter your phone number. We will send you a confirmation code there',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row( Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row(

View File

@ -19,7 +19,6 @@ import 'onboarding_viewmodel.dart';
import 'onboarding_view.form.dart'; import 'onboarding_view.form.dart';
@FormView(fields: [ @FormView(fields: [
FormTextField(name: 'answer', validator: FormValidator.validateForm),
FormTextField(name: 'fullName', validator: FormValidator.validateForm), FormTextField(name: 'fullName', validator: FormValidator.validateForm),
FormTextField(name: 'challenge', validator: FormValidator.validateForm), FormTextField(name: 'challenge', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm), FormTextField(name: 'occupation', validator: FormValidator.validateForm),
@ -30,13 +29,55 @@ class OnboardingView extends StackedView<OnboardingViewModel>
with $OnboardingView { with $OnboardingView {
const OnboardingView({Key? key}) : super(key: key); const OnboardingView({Key? key}) : super(key: key);
void _initFormFields() { void _initClearData() {
answerController.text = 'Book'; topicController.clear();
fullNameController.clear();
challengeController.clear();
occupationController.clear();
languageGoalController.clear();
}
void _clearDataOnNavigation(OnboardingViewModel viewModel) {
if (viewModel.currentPage == 0) {
fullNameController.clear();
viewModel.resetFullNameFormScreen();
} else if (viewModel.currentPage == 1) {
viewModel.resetGenderFormScreen();
} else if (viewModel.currentPage == 2) {
viewModel.resetBirthdayFormScreen();
} else if (viewModel.currentPage == 3) {
viewModel.resetAgeGroupFormScreen();
} else if (viewModel.currentPage == 4) {
viewModel.resetEducationalBackgroundFormScreen();
} else if (viewModel.currentPage == 5) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
} else if (viewModel.currentPage == 6) {
viewModel.resetCountryRegionFormScreen();
} else if (viewModel.currentPage == 7) {
viewModel.resetLearningGoalFormScreen();
} else if (viewModel.currentPage == 8) {
languageGoalController.clear();
viewModel.resetLanguageGoalFormScreen();
} else if (viewModel.currentPage == 9) {
challengeController.clear();
viewModel.resetChallengeFormScreen();
} else if (viewModel.currentPage == 10) {
topicController.clear();
viewModel.resetTopicFormScreen();
}
}
void _pop(OnboardingViewModel viewModel) {
{
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
} }
@override @override
void onViewModelReady(OnboardingViewModel viewModel) { void onViewModelReady(OnboardingViewModel viewModel) {
_initFormFields(); _initClearData();
syncFormWithViewModel(viewModel); syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -57,8 +98,8 @@ class OnboardingView extends StackedView<OnboardingViewModel>
Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) => Widget _buildOnboardingScreensWrapper(OnboardingViewModel viewModel) =>
PopScope( PopScope(
canPop: false, canPop: viewModel.currentPage == 0 ? true : false,
onPopInvokedWithResult: (value, data) => viewModel.pop(), onPopInvokedWithResult: (value, data) => _pop(viewModel),
child: _buildOnboardingScreens(viewModel)); child: _buildOnboardingScreens(viewModel));
Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack( Widget _buildOnboardingScreens(OnboardingViewModel viewModel) => IndexedStack(

View File

@ -12,7 +12,6 @@ import 'package:yimaru_app/ui/common/validators/form_validator.dart';
const bool _autoTextFieldValidation = true; const bool _autoTextFieldValidation = true;
const String AnswerValueKey = 'answer';
const String FullNameValueKey = 'fullName'; const String FullNameValueKey = 'fullName';
const String ChallengeValueKey = 'challenge'; const String ChallengeValueKey = 'challenge';
const String OccupationValueKey = 'occupation'; const String OccupationValueKey = 'occupation';
@ -25,7 +24,6 @@ final Map<String, TextEditingController> _OnboardingViewTextEditingControllers =
final Map<String, FocusNode> _OnboardingViewFocusNodes = {}; final Map<String, FocusNode> _OnboardingViewFocusNodes = {};
final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = { final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
AnswerValueKey: FormValidator.validateForm,
FullNameValueKey: FormValidator.validateForm, FullNameValueKey: FormValidator.validateForm,
ChallengeValueKey: FormValidator.validateForm, ChallengeValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm, OccupationValueKey: FormValidator.validateForm,
@ -34,8 +32,6 @@ final Map<String, String? Function(String?)?> _OnboardingViewTextValidations = {
}; };
mixin $OnboardingView { mixin $OnboardingView {
TextEditingController get answerController =>
_getFormTextEditingController(AnswerValueKey);
TextEditingController get fullNameController => TextEditingController get fullNameController =>
_getFormTextEditingController(FullNameValueKey); _getFormTextEditingController(FullNameValueKey);
TextEditingController get challengeController => TextEditingController get challengeController =>
@ -47,7 +43,6 @@ mixin $OnboardingView {
TextEditingController get topicController => TextEditingController get topicController =>
_getFormTextEditingController(TopicValueKey); _getFormTextEditingController(TopicValueKey);
FocusNode get answerFocusNode => _getFormFocusNode(AnswerValueKey);
FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey); FocusNode get fullNameFocusNode => _getFormFocusNode(FullNameValueKey);
FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey); FocusNode get challengeFocusNode => _getFormFocusNode(ChallengeValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey); FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
@ -79,7 +74,6 @@ mixin $OnboardingView {
/// Registers a listener on every generated controller that calls [model.setData()] /// Registers a listener on every generated controller that calls [model.setData()]
/// with the latest textController values /// with the latest textController values
void syncFormWithViewModel(FormStateHelper model) { void syncFormWithViewModel(FormStateHelper model) {
answerController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model)); fullNameController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model)); challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model)); occupationController.addListener(() => _updateFormData(model));
@ -96,7 +90,6 @@ mixin $OnboardingView {
'This feature was deprecated after 3.1.0.', 'This feature was deprecated after 3.1.0.',
) )
void listenToFormUpdated(FormViewModel model) { void listenToFormUpdated(FormViewModel model) {
answerController.addListener(() => _updateFormData(model));
fullNameController.addListener(() => _updateFormData(model)); fullNameController.addListener(() => _updateFormData(model));
challengeController.addListener(() => _updateFormData(model)); challengeController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model)); occupationController.addListener(() => _updateFormData(model));
@ -111,7 +104,6 @@ mixin $OnboardingView {
model.setData( model.setData(
model.formValueMap model.formValueMap
..addAll({ ..addAll({
AnswerValueKey: answerController.text,
FullNameValueKey: fullNameController.text, FullNameValueKey: fullNameController.text,
ChallengeValueKey: challengeController.text, ChallengeValueKey: challengeController.text,
OccupationValueKey: occupationController.text, OccupationValueKey: occupationController.text,
@ -158,7 +150,6 @@ extension ValueProperties on FormStateHelper {
return !hasAnyValidationMessage; return !hasAnyValidationMessage;
} }
String? get answerValue => this.formValueMap[AnswerValueKey] as String?;
String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?; String? get fullNameValue => this.formValueMap[FullNameValueKey] as String?;
String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?; String? get challengeValue => this.formValueMap[ChallengeValueKey] as String?;
String? get occupationValue => String? get occupationValue =>
@ -167,16 +158,6 @@ extension ValueProperties on FormStateHelper {
this.formValueMap[LanguageGoalValueKey] as String?; this.formValueMap[LanguageGoalValueKey] as String?;
String? get topicValue => this.formValueMap[TopicValueKey] as String?; String? get topicValue => this.formValueMap[TopicValueKey] as String?;
set answerValue(String? value) {
this.setData(
this.formValueMap..addAll({AnswerValueKey: value}),
);
if (_OnboardingViewTextEditingControllers.containsKey(AnswerValueKey)) {
_OnboardingViewTextEditingControllers[AnswerValueKey]?.text = value ?? '';
}
}
set fullNameValue(String? value) { set fullNameValue(String? value) {
this.setData( this.setData(
this.formValueMap..addAll({FullNameValueKey: value}), this.formValueMap..addAll({FullNameValueKey: value}),
@ -232,9 +213,6 @@ extension ValueProperties on FormStateHelper {
} }
} }
bool get hasAnswer =>
this.formValueMap.containsKey(AnswerValueKey) &&
(answerValue?.isNotEmpty ?? false);
bool get hasFullName => bool get hasFullName =>
this.formValueMap.containsKey(FullNameValueKey) && this.formValueMap.containsKey(FullNameValueKey) &&
(fullNameValue?.isNotEmpty ?? false); (fullNameValue?.isNotEmpty ?? false);
@ -251,8 +229,6 @@ extension ValueProperties on FormStateHelper {
this.formValueMap.containsKey(TopicValueKey) && this.formValueMap.containsKey(TopicValueKey) &&
(topicValue?.isNotEmpty ?? false); (topicValue?.isNotEmpty ?? false);
bool get hasAnswerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey]?.isNotEmpty ?? false;
bool get hasFullNameValidationMessage => bool get hasFullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false; this.fieldsValidationMessages[FullNameValueKey]?.isNotEmpty ?? false;
bool get hasChallengeValidationMessage => bool get hasChallengeValidationMessage =>
@ -264,8 +240,6 @@ extension ValueProperties on FormStateHelper {
bool get hasTopicValidationMessage => bool get hasTopicValidationMessage =>
this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false; this.fieldsValidationMessages[TopicValueKey]?.isNotEmpty ?? false;
String? get answerValidationMessage =>
this.fieldsValidationMessages[AnswerValueKey];
String? get fullNameValidationMessage => String? get fullNameValidationMessage =>
this.fieldsValidationMessages[FullNameValueKey]; this.fieldsValidationMessages[FullNameValueKey];
String? get challengeValidationMessage => String? get challengeValidationMessage =>
@ -279,8 +253,6 @@ extension ValueProperties on FormStateHelper {
} }
extension Methods on FormStateHelper { extension Methods on FormStateHelper {
setAnswerValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[AnswerValueKey] = validationMessage;
setFullNameValidationMessage(String? validationMessage) => setFullNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FullNameValueKey] = validationMessage; this.fieldsValidationMessages[FullNameValueKey] = validationMessage;
setChallengeValidationMessage(String? validationMessage) => setChallengeValidationMessage(String? validationMessage) =>
@ -294,7 +266,6 @@ extension Methods on FormStateHelper {
/// Clears text input fields on the Form /// Clears text input fields on the Form
void clearForm() { void clearForm() {
answerValue = '';
fullNameValue = ''; fullNameValue = '';
challengeValue = ''; challengeValue = '';
occupationValue = ''; occupationValue = '';
@ -305,7 +276,6 @@ extension Methods on FormStateHelper {
/// Validates text input fields on the Form /// Validates text input fields on the Form
void validateForm() { void validateForm() {
this.setValidationMessages({ this.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey), FullNameValueKey: getValidationMessage(FullNameValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey), ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey), OccupationValueKey: getValidationMessage(OccupationValueKey),
@ -330,7 +300,6 @@ String? getValidationMessage(String key) {
/// Updates the fieldsValidationMessages on the FormViewModel /// Updates the fieldsValidationMessages on the FormViewModel
void updateValidationData(FormStateHelper model) => void updateValidationData(FormStateHelper model) =>
model.setValidationMessages({ model.setValidationMessages({
AnswerValueKey: getValidationMessage(AnswerValueKey),
FullNameValueKey: getValidationMessage(FullNameValueKey), FullNameValueKey: getValidationMessage(FullNameValueKey),
ChallengeValueKey: getValidationMessage(ChallengeValueKey), ChallengeValueKey: getValidationMessage(ChallengeValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey), OccupationValueKey: getValidationMessage(OccupationValueKey),

View File

@ -54,10 +54,13 @@ class OnboardingViewModel extends FormViewModel {
// Age group // Age group
final List<String> _ageGroups = [ final List<String> _ageGroups = [
'8-14', 'UNDER_13',
'15-18', '13_17',
'19-26', '18_24',
'26+', '25_34',
'35_44',
'45_54',
'55_PLUS'
]; ];
List<String> get ageGroups => _ageGroups; List<String> get ageGroups => _ageGroups;
@ -76,30 +79,11 @@ class OnboardingViewModel extends FormViewModel {
String get selectedCountry => _selectedCountry; String get selectedCountry => _selectedCountry;
Future<List<String>> getCountries() async => ['Ethiopia'];
// Country // Country
String _selectedRegion = 'Addis Ababa'; String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion; String get selectedRegion => _selectedRegion;
Future<List<String>> getRegions(String country) async => [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
// Learning goal // Learning goal
String? _selectedLearningGoal; String? _selectedLearningGoal;
@ -258,12 +242,42 @@ class OnboardingViewModel extends FormViewModel {
} }
// Country // Country
List<String> getCountries() => ['Ethiopia', 'Other'];
void setSelectedCountry(String value) { void setSelectedCountry(String value) {
_selectedCountry = value; _selectedCountry = value;
if (selectedCountry != 'Ethiopia') {
_selectedRegion = 'Other';
} else {
_selectedRegion = 'Addis Ababa';
}
rebuildUi(); rebuildUi();
} }
// Region // Region
List<String> getRegions(String country) {
if (country == 'Ethiopia') {
return [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
} else {
return ['Other'];
}
}
void setSelectedRegion(String value) { void setSelectedRegion(String value) {
_selectedRegion = value; _selectedRegion = value;
rebuildUi(); rebuildUi();
@ -352,24 +366,89 @@ class OnboardingViewModel extends FormViewModel {
// Add user data // Add user data
void addUserData(Map<String, dynamic> data) { void addUserData(Map<String, dynamic> data) {
_userData.addAll(data); _userData.addAll(data);
print('User data : $_userData');
} }
void clearUserData() { void clearUserData() {
_userData.clear(); _userData.clear();
} }
// Navigation // Form reset
Future<void> navigateToLanguage() async => // Reset full name form screen
await _navigationService.navigateToLanguageView(); void resetFullNameFormScreen() {
_focusFullName = false;
rebuildUi();
}
Future<void> navigateToAssessment() async => // Reset gender form screen
await _navigationService.navigateToAssessmentView(data: _userData); void resetGenderFormScreen() {
_selectedGender = null;
rebuildUi();
}
Future<void> replaceWithHome() async => // Reset birthday form screen
await _navigationService.clearStackAndShowView(const HomeView()); void resetBirthdayFormScreen() {
_selectedBirthday = null;
rebuildUi();
}
// Reset age group form screen
void resetAgeGroupFormScreen() {
_selectedAgeGroup = null;
rebuildUi();
}
// Reset educational background form screen
void resetEducationalBackgroundFormScreen() {
_selectedEducationalBackground = null;
rebuildUi();
}
// Reset occupation form screen
void resetOccupationFormScreen() {
_focusOccupation = false;
rebuildUi();
}
// Reset country region form screen
void resetCountryRegionFormScreen() {
_selectedCountry = 'Ethiopia';
_selectedRegion = 'Addis Ababa';
rebuildUi();
}
// Reset learning goal form screen
void resetLearningGoalFormScreen() {
_selectedLearningGoal = null;
rebuildUi();
}
// Reset language goal form screen
void resetLanguageGoalFormScreen() {
_focusLanguageGoal = false;
_selectedLanguageGoal = null;
_showLanguageGoalTextBox = false;
rebuildUi();
}
// Reset challenge form screen
void resetChallengeFormScreen() {
_focusChallenge = false;
_selectedChallenge = null;
_showChallengeTextBox = false;
rebuildUi();
}
// Reset topic form screen
void resetTopicFormScreen() {
_focusTopic = false;
_selectedTopic = null;
_showTopicTextBox = false;
rebuildUi();
}
// In-app navigation
void next({int? page}) async { void next({int? page}) async {
if (page == null) { if (page == null) {
if (_previousPage != 0) { if (_previousPage != 0) {
@ -384,8 +463,8 @@ class OnboardingViewModel extends FormViewModel {
rebuildUi(); rebuildUi();
} }
void pop() { void goBack() {
if (_currentPage == 8) { if (_currentPage == 0) {
_navigationService.back(); _navigationService.back();
} else { } else {
_currentPage--; _currentPage--;
@ -393,4 +472,14 @@ class OnboardingViewModel extends FormViewModel {
rebuildUi(); rebuildUi();
} }
} }
// Navigation
Future<void> navigateToLanguage() async =>
await _navigationService.navigateToLanguageView();
Future<void> navigateToAssessment() async =>
await _navigationService.navigateToAssessmentView(data: _userData);
Future<void> replaceWithHome() async =>
await _navigationService.clearStackAndShowView(const HomeView());
} }

View File

@ -10,6 +10,12 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> { class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
const AgeGroupFormScreen({super.key}); const AgeGroupFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetAgeGroupFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -74,24 +80,20 @@ class AgeGroupFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Which age range are you in?', 'Which age range are you in?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubTitle() => Text(
'Well personalize your learning experience based on your age.', 'Well personalize your learning experience based on your age.',
style: TextStyle(color: kcMediumGrey), style: style14DG400,
); );
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder( Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -12,14 +12,16 @@ import '../../../widgets/birthday_selector.dart';
class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> { class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
const BirthdayFormScreen({super.key}); const BirthdayFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetBirthdayFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = { Map<String, dynamic> data = {'birth_day': viewModel.selectedBirthday};
'birth_day': DateFormat('yyyy-MM-dd')
.parseUTC(viewModel.selectedBirthday ?? DateTime.now().toString())
.toIso8601String()
};
viewModel.addUserData(data); viewModel.addUserData(data);
viewModel.next(); viewModel.next();
} }
@ -68,30 +70,26 @@ class BirthdayFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildBirthdayFormField(viewModel) _buildBirthdayFormField(viewModel)
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Pick your birthday?', 'Pick your birthday?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your birthday.', 'Well personalize your learning experience based on your birthday.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildBirthdayFormField(OnboardingViewModel viewModel) => Widget _buildBirthdayFormField(OnboardingViewModel viewModel) =>

View File

@ -13,6 +13,12 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
const ChallengeFormScreen({super.key, required this.challengeController}); const ChallengeFormScreen({super.key, required this.challengeController});
void _pop(OnboardingViewModel viewModel) {
challengeController.clear();
viewModel.resetChallengeFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -74,7 +80,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildChallenges(viewModel), _buildChallenges(viewModel),
if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel), if (viewModel.showChallengeTextBox) _buildChallengeFormField(viewModel),
@ -90,24 +96,20 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'What challenge do you face most with English?', 'What challenge do you face most with English?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Everyone has struggles, lets start fixing yours 😊', 'Everyone has struggles, lets start fixing yours 😊',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder( Widget _buildChallenges(OnboardingViewModel viewModel) => ListView.builder(
@ -151,11 +153,7 @@ class ChallengeFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text( Widget _buildChallengeValidator(OnboardingViewModel viewModel) => Text(
viewModel.challengeValidationMessage!, viewModel.challengeValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> { class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
const CountryRegionFormScreen({super.key}); const CountryRegionFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetCountryRegionFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -71,7 +76,7 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildCountryDropDown(viewModel), _buildCountryDropDown(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
@ -80,31 +85,27 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Where are you from?', 'Where are you from?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Select your country and region from the dropdown', 'Select your country and region from the dropdown',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildCountryDropDown(OnboardingViewModel viewModel) => Widget _buildCountryDropDown(OnboardingViewModel viewModel) =>
CustomDropdownPicker( CustomDropdownPicker(
hint: 'Select country', hint: 'Select country',
icon: _buildSearchIcon(), icon: _buildSearchIcon(),
selectedItem: 'Ethiopia', selectedItem: viewModel.selectedCountry,
items: (value, props) => viewModel.getCountries(), items: (value, props) => viewModel.getCountries(),
onChanged: (value) => onChanged: (value) =>
viewModel.setSelectedCountry(value ?? 'Ethiopia')); viewModel.setSelectedCountry(value ?? 'Ethiopia'));
@ -113,10 +114,11 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
CustomDropdownPicker( CustomDropdownPicker(
hint: 'Select region', hint: 'Select region',
icon: _buildSearchIcon(), icon: _buildSearchIcon(),
selectedItem: 'Addis Ababa', selectedItem: viewModel.selectedRegion,
items: (value, props) =>
viewModel.getRegions(viewModel.selectedCountry),
onChanged: (value) => onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'), viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
items: (value, props) => viewModel.getRegions('Addis Ababa'),
); );
Icon _buildSearchIcon() => const Icon( Icon _buildSearchIcon() => const Icon(

View File

@ -11,6 +11,11 @@ class EducationalBackgroundFormScreen
extends ViewModelWidget<OnboardingViewModel> { extends ViewModelWidget<OnboardingViewModel> {
const EducationalBackgroundFormScreen({super.key}); const EducationalBackgroundFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetEducationalBackgroundFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -77,9 +82,9 @@ class EducationalBackgroundFormScreen
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );

View File

@ -72,7 +72,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceLarge, verticalSpaceLarge,
_buildFullNameFormField(viewModel), _buildFullNameFormField(viewModel),
if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName) if (viewModel.hasFullNameValidationMessage && viewModel.focusFullName)
@ -96,7 +96,7 @@ class FullNameFormScreen extends ViewModelWidget<OnboardingViewModel> {
), ),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => const Text(
'Well use your name to personalize your learning journey.', 'Well use your name to personalize your learning journey.',
style: TextStyle(color: kcMediumGrey), style: TextStyle(color: kcMediumGrey),
); );

View File

@ -10,6 +10,11 @@ import 'package:yimaru_app/ui/widgets/large_app_bar.dart';
class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> { class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
const GenderFormScreen({super.key}); const GenderFormScreen({super.key});
void _pop(OnboardingViewModel viewModel) {
viewModel.resetGenderFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -63,30 +68,26 @@ class GenderFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildAgeGroups(viewModel) _buildAgeGroups(viewModel)
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Choose your gender?', 'Choose your gender?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your gender.', 'Well personalize your learning experience based on your gender.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder( Widget _buildAgeGroups(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -14,6 +14,12 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
const LanguageGoalFormScreen( const LanguageGoalFormScreen(
{super.key, required this.languageGoalController}); {super.key, required this.languageGoalController});
void _pop(OnboardingViewModel viewModel) {
languageGoalController.clear();
viewModel.resetLanguageGoalFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -75,7 +81,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildReasons(viewModel), _buildReasons(viewModel),
if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel), if (viewModel.showLanguageGoalTextBox) _buildReasonFormField(viewModel),
@ -91,26 +97,20 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Whats your main goal for improving your English?', 'Whats your main goal for improving your English?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Your goal helps us tailor your learning journey.', 'Your goal helps us tailor your learning journey.',
style: TextStyle( style: style14MG400,
color: kcMediumGrey,
),
); );
Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder( Widget _buildReasons(OnboardingViewModel viewModel) => ListView.builder(
@ -154,11 +154,7 @@ class LanguageGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text( Widget _buildReasonValidator(OnboardingViewModel viewModel) => Text(
viewModel.languageGoalValidationMessage!, viewModel.languageGoalValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -23,6 +23,11 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
return Icons.book; return Icons.book;
} }
void _pop(OnboardingViewModel viewModel) {
viewModel.resetLearningGoalFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -87,19 +92,22 @@ class LearningGoalFormScreen extends ViewModelWidget<OnboardingViewModel> {
]; ];
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
onPop: viewModel.pop,
showBackButton: true, showBackButton: true,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle(OnboardingViewModel viewModel) => Text( Widget _buildTitle(OnboardingViewModel viewModel) => Text.rich(
'Hi ${viewModel.userData['first_name']}, Choose your learning goal.', TextSpan(
style: const TextStyle( text: 'Hi ${viewModel.userData['first_name']},',
fontSize: 25, style: style18P600.copyWith(fontSize: 22),
color: kcDarkGrey, children: [
fontWeight: FontWeight.w600, TextSpan(
), text: ' Choose your learning goal.',
style: style16DG600.copyWith(fontSize: 22),
)
]),
); );
Widget _buildLearningGoals(OnboardingViewModel viewModel) => ListView.builder( Widget _buildLearningGoals(OnboardingViewModel viewModel) => ListView.builder(

View File

@ -13,6 +13,12 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
const OccupationFormScreen({super.key, required this.occupationController}); const OccupationFormScreen({super.key, required this.occupationController});
void _pop(OnboardingViewModel viewModel) {
occupationController.clear();
viewModel.resetOccupationFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -71,7 +77,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceLarge, verticalSpaceLarge,
_buildOccupationFormField(viewModel), _buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage && if (viewModel.hasOccupationValidationMessage &&
@ -84,23 +90,19 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Whats your occupation?', 'Whats your occupation?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Well personalize your learning experience based on your occupation.', 'Well personalize your learning experience based on your occupation.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildOccupationFormField(OnboardingViewModel viewModel) => Widget _buildOccupationFormField(OnboardingViewModel viewModel) =>
@ -120,11 +122,7 @@ class OccupationFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text( Widget _buildOccupationValidator(OnboardingViewModel viewModel) => Text(
viewModel.occupationValidationMessage!, viewModel.occupationValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -13,10 +13,18 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
const TopicFormScreen({super.key, required this.topicController}); const TopicFormScreen({super.key, required this.topicController});
void _pop(OnboardingViewModel viewModel) {
topicController.clear();
viewModel.resetTopicFormScreen();
viewModel.goBack();
}
Future<void> _next(OnboardingViewModel viewModel) async { Future<void> _next(OnboardingViewModel viewModel) async {
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
Map<String, dynamic> data = { Map<String, dynamic> data = {
'profile_completed': true,
'preferred_language': 'en',
'favoutite_topic': viewModel.selectedTopic ?? topicController.text, 'favoutite_topic': viewModel.selectedTopic ?? topicController.text,
}; };
viewModel.addUserData(data); viewModel.addUserData(data);
@ -43,8 +51,8 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar(
showBackButton: true, showBackButton: true,
onPop: viewModel.pop,
showLanguageSelection: true, showLanguageSelection: true,
onPop: () => _pop(viewModel),
onLanguage: () async => await viewModel.navigateToLanguage(), onLanguage: () async => await viewModel.navigateToLanguage(),
); );
@ -80,7 +88,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
_buildTitle(), _buildTitle(),
verticalSpaceSmall, verticalSpaceSmall,
_buildSubTitle(), _buildSubtitle(),
verticalSpaceMedium, verticalSpaceMedium,
_buildTopics(viewModel), _buildTopics(viewModel),
if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel), if (viewModel.showTopicTextBox) _buildTopicFormField(viewModel),
@ -95,18 +103,14 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
verticalSpaceMedium, verticalSpaceMedium,
]; ];
Widget _buildTitle() => const Text( Widget _buildTitle() => Text(
'Which topics interest you most?', 'Which topics interest you most?',
style: TextStyle( style: style25DG600,
fontSize: 25,
color: kcDarkGrey,
fontWeight: FontWeight.w600,
),
); );
Widget _buildSubTitle() => const Text( Widget _buildSubtitle() => Text(
'Your favorite topics help us create fun, relatable lessons.', 'Your favorite topics help us create fun, relatable lessons.',
style: TextStyle(color: kcMediumGrey), style: style14MG400,
); );
Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder( Widget _buildTopics(OnboardingViewModel viewModel) => ListView.builder(
@ -148,11 +152,7 @@ class TopicFormScreen extends ViewModelWidget<OnboardingViewModel> {
Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text( Widget _buildTopicValidator(OnboardingViewModel viewModel) => Text(
viewModel.topicValidationMessage!, viewModel.topicValidationMessage!,
style: const TextStyle( style: style12R700,
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
); );
Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding( Widget _buildContinueButtonWrapper(OnboardingViewModel viewModel) => Padding(

View File

@ -1,17 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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/common/enmus.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/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';
import '../../widgets/custom_elevated_button.dart'; import '../../widgets/custom_elevated_button.dart';
import '../../widgets/image_picker_option.dart';
import 'profile_viewmodel.dart'; import 'profile_viewmodel.dart';
class ProfileView extends StackedView<ProfileViewModel> { class ProfileView extends StackedView<ProfileViewModel> {
const ProfileView({Key? key}) : super(key: key); const ProfileView({Key? key}) : super(key: key);
Future<void> _showImagePicker(
{required BuildContext context,
required ProfileViewModel viewModel}) async =>
await showDialog(
context: context,
builder: (context) =>
_showImagePickerDialog(context: context, viewModel: viewModel),
);
AlertDialog _showImagePickerDialog(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
AlertDialog(
backgroundColor: Colors.transparent,
content: _buildImagePicker(context: context, viewModel: viewModel),
);
@override @override
ProfileViewModel viewModelBuilder( ProfileViewModel viewModelBuilder(
BuildContext context, BuildContext context,
@ -24,30 +43,45 @@ class ProfileView extends StackedView<ProfileViewModel> {
ProfileViewModel viewModel, ProfileViewModel viewModel,
Widget? child, Widget? child,
) => ) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffold(context: context, viewModel: viewModel),
); );
Widget _buildScaffold(ProfileViewModel viewModel) => Widget _buildScaffold(
SafeArea(child: _buildBodyWrapper(viewModel)); {required BuildContext context,
required ProfileViewModel viewModel}) =>
SafeArea(
child: _buildBodyWrapper(context: context, viewModel: viewModel));
Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView( Widget _buildBodyWrapper(
child: _buildBody(viewModel), {required BuildContext context,
required ProfileViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBody(context: context, viewModel: viewModel),
); );
Widget _buildBody(ProfileViewModel viewModel) => Padding( Widget _buildBody(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel), child: _buildColumn(context: context, viewModel: viewModel),
); );
Widget _buildColumn(ProfileViewModel viewModel) => Column( Widget _buildColumn(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Column(
children: [ children: [
verticalSpaceMedium, verticalSpaceMedium,
_buildNotificationIconWrapper(), _buildNotificationIconWrapper(),
_buildProfileSection(), _buildProfileSection(context: context, viewModel: viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildViewProfileButton(viewModel), _buildViewProfileButton(viewModel),
verticalSpaceLarge, verticalSpaceLarge,
@ -66,27 +100,46 @@ class ProfileView extends StackedView<ProfileViewModel> {
color: kcDarkGrey, color: kcDarkGrey,
); );
Widget _buildProfileSection() => Column( Widget _buildProfileSection(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: _buildProfileSectionChildren(), children: _buildProfileSectionChildren(
context: context, viewModel: viewModel),
); );
List<Widget> _buildProfileSectionChildren() => [ List<Widget> _buildProfileSectionChildren(
_buildProfileImage(), {required BuildContext context,
required ProfileViewModel viewModel}) =>
[
_buildProfileImage(context: context, viewModel: viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildProfileName(), _buildProfileName(viewModel),
]; ];
Widget _buildProfileImage() => const ProfileImage(); Widget _buildProfileImage(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
ProfileImage(
profileImage: viewModel.user?.profilePicture,
loading: viewModel.busy(StateObjects.profileImage) ? true : false,
onTap: () async =>
await _showImagePicker(context: context, viewModel: viewModel),
);
Widget _buildProfileName() => const Text( Widget _buildImagePicker(
'Hi, Bisrat 👋', {required BuildContext context,
style: TextStyle( required ProfileViewModel viewModel}) =>
fontSize: 25, ImagePickerOption(
color: kcDarkGrey, onCameraTap: () async => await viewModel.openCamera(),
fontWeight: FontWeight.w600, onGalleryTap: () async => await viewModel.openGallery(),
), );
Widget _buildProfileName(ProfileViewModel viewModel) => Text(
'Hi, ${viewModel.user?.firstName ?? ''} 👋',
style: style25DG600,
); );
Widget _buildViewProfileButton(ProfileViewModel viewModel) => Widget _buildViewProfileButton(ProfileViewModel viewModel) =>
@ -111,28 +164,28 @@ class ProfileView extends StackedView<ProfileViewModel> {
Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard(
icon: Icons.download, icon: Icons.download,
title: 'My Downloads', title: 'My Downloads',
subTitle: 'Access offline lessons and saved videos', subtitle: 'Access offline lessons and saved videos',
onTap: () async => await viewModel.navigateToDownloads(), onTap: () async => await viewModel.navigateToDownloads(),
); );
Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard(
title: 'My Progress', title: 'My Progress',
icon: Icons.stacked_bar_chart, icon: Icons.stacked_bar_chart,
subTitle: 'Track your achievements and learning streak', subtitle: 'Track your achievements and learning streak',
onTap: () async => await viewModel.navigateToProgress(), onTap: () async => await viewModel.navigateToProgress(),
); );
Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Account & Privacy', title: 'Account & Privacy',
icon: Icons.privacy_tip_outlined, icon: Icons.privacy_tip_outlined,
subTitle: 'Manage setting and app preference', subtitle: 'Manage setting and app preference',
onTap: () async => await viewModel.navigateToAccountPrivacy(), onTap: () async => await viewModel.navigateToAccountPrivacy(),
); );
Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard( Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard(
title: 'Support', title: 'Support',
icon: Icons.headphones, icon: Icons.headphones,
subTitle: 'Get help through phone or Telegram', subtitle: 'Get help through phone or Telegram',
onTap: () async => await viewModel.navigateToSupport(), onTap: () async => await viewModel.navigateToSupport(),
); );
@ -141,7 +194,7 @@ class ProfileView extends StackedView<ProfileViewModel> {
text: 'Log Out', text: 'Log Out',
borderRadius: 12, borderRadius: 12,
foregroundColor: kcRed, foregroundColor: kcRed,
onTap: () async => await viewModel.logOut(),
backgroundColor: kcRed.withOpacity(0.25), backgroundColor: kcRed.withOpacity(0.25),
onTap: () async => await viewModel.logOut(),
); );
} }

View File

@ -1,20 +1,93 @@
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart'; import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart'; import '../../../services/authentication_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/app_colors.dart';
class ProfileViewModel extends ReactiveViewModel {
final _apiService = locator<ApiService>();
final _dialogService = locator<DialogService>();
final _statusChecker = locator<StatusCheckerService>();
class ProfileViewModel extends BaseViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>(); final _authenticationService = locator<AuthenticationService>();
Future<void> logOut() async { @override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// Image picker
Future<void> openCamera() async =>
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
Future<void> _openCamera() async {
String? image = await _imagePickerService.camera();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
Future<void> openGallery() async =>
runBusyFuture(_openGallery(), busyObject: StateObjects.profileImage);
Future<void> _openGallery() async {
String? image = await _imagePickerService.gallery();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
// Logout
Future<void> _logOut() async {
await _authenticationService.logOut(); await _authenticationService.logOut();
await _navigationService.replaceWithLoginView(); await _navigationService.replaceWithLoginView();
} }
// Dialog
Future<bool?> showAbortDialog() async {
DialogResponse? response = await _dialogService.showDialog(
title: 'Logout',
cancelTitle: 'No',
buttonTitle: 'Yes',
barrierDismissible: true,
cancelTitleColor: kcDarkGrey,
buttonTitleColor: kcPrimaryColor,
description: 'Are you sure you want to quit?',
);
return response?.confirmed;
}
Future<void> logOut() async {
bool? response = await showAbortDialog();
if (response != null && response) {
await _logOut();
}
}
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToProfileDetail() async => Future<void> navigateToProfileDetail() async =>
await _navigationService.navigateToProfileDetailView(); await _navigationService.navigateToProfileDetailView();
@ -29,4 +102,19 @@ class ProfileViewModel extends BaseViewModel {
Future<void> navigateToSupport() async => Future<void> navigateToSupport() async =>
await _navigationService.navigateToSupportView(); await _navigationService.navigateToSupportView();
// Remote api call
// Update profile
Future<void> updateProfilePicture(String image) async =>
await runBusyFuture(_updateProfilePicture(image));
Future<void> _updateProfilePicture(String image) async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> data = {
'profile_picture_url': image,
};
await _apiService.updateProfileImage(data: data, userId: _user?.userId);
}
}
} }

View File

@ -8,10 +8,13 @@ import 'package:yimaru_app/ui/widgets/custom_form_label.dart';
import 'package:yimaru_app/ui/widgets/small_app_bar.dart'; import 'package:yimaru_app/ui/widgets/small_app_bar.dart';
import '../../common/app_colors.dart'; import '../../common/app_colors.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart'; import '../../common/ui_helpers.dart';
import '../../common/validators/form_validator.dart'; import '../../common/validators/form_validator.dart';
import '../../widgets/custom_dropdown.dart'; import '../../widgets/custom_dropdown.dart';
import '../../widgets/custom_elevated_button.dart'; import '../../widgets/custom_elevated_button.dart';
import '../../widgets/image_picker_option.dart';
import '../../widgets/page_loading_indicator.dart';
import '../../widgets/profile_image.dart'; import '../../widgets/profile_image.dart';
import 'profile_detail_viewmodel.dart'; import 'profile_detail_viewmodel.dart';
@ -23,21 +26,62 @@ import 'profile_detail_view.form.dart';
name: 'phoneNumber', validator: FormValidator.validatePhoneNumber), name: 'phoneNumber', validator: FormValidator.validatePhoneNumber),
FormTextField(name: 'lastName', validator: FormValidator.validateForm), FormTextField(name: 'lastName', validator: FormValidator.validateForm),
FormTextField(name: 'firstName', validator: FormValidator.validateForm), FormTextField(name: 'firstName', validator: FormValidator.validateForm),
FormTextField(name: 'occupation', validator: FormValidator.validateForm),
]) ])
class ProfileDetailView extends StackedView<ProfileDetailViewModel> class ProfileDetailView extends StackedView<ProfileDetailViewModel>
with $ProfileDetailView { with $ProfileDetailView {
const ProfileDetailView({Key? key}) : super(key: key); const ProfileDetailView({Key? key}) : super(key: key);
void _onModelReady() { Future<void> _update(ProfileDetailViewModel viewModel) async {
firstNameController.text = 'Abel'; Map<String, dynamic> data = {
lastNameController.text = 'Abebe'; 'region': viewModel.selectedRegion,
'gender': viewModel.selectedGender,
'last_name': lastNameController.text,
'country': viewModel.selectedCountry,
'first_name': firstNameController.text,
'occupation': occupationController.text,
'birth_day': viewModel.selectedBirthday,
};
viewModel.addUserData(data);
await viewModel.updateProfile();
}
Future<void> _showImagePicker(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) async =>
await showDialog(
context: context,
builder: (context) =>
_showImagePickerDialog(context: context, viewModel: viewModel),
);
AlertDialog _showImagePickerDialog(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
AlertDialog(
backgroundColor: Colors.transparent,
content: _buildImagePicker(context: context, viewModel: viewModel),
);
void _onModelReady(ProfileDetailViewModel viewModel) {
phoneNumberController.text = '251900000000'; phoneNumberController.text = '251900000000';
emailController.text = 'email@test.com'; emailController.text = viewModel.user?.email ?? '';
lastNameController.text = viewModel.user?.lastName ?? '';
firstNameController.text = viewModel.user?.firstName ?? '';
occupationController.text = viewModel.user?.occupation ?? '';
viewModel.clearUserData();
viewModel.setGender(viewModel.user?.gender ?? '');
viewModel.setSelectedCountry(viewModel.user?.country ?? 'Ethiopia');
viewModel.setSelectedRegion(viewModel.user?.region ?? 'Addis Ababa');
viewModel.setBirthday(viewModel.user?.birthday ??
DateFormat('d MMM, yyyy').format(DateTime.now()));
} }
@override @override
void onViewModelReady(ProfileDetailViewModel viewModel) { void onViewModelReady(ProfileDetailViewModel viewModel) {
_onModelReady(); _onModelReady(viewModel);
syncFormWithViewModel(viewModel); syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -52,32 +96,55 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
ProfileDetailViewModel viewModel, ProfileDetailViewModel viewModel,
Widget? child, Widget? child,
) => ) =>
_buildScaffoldWrapper(viewModel); _buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(ProfileDetailViewModel viewModel) => Scaffold( Widget _buildScaffoldWrapper(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor, backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel), body: _buildScaffoldStack(context: context, viewModel: viewModel),
); );
Widget _buildScaffold(ProfileDetailViewModel viewModel) => Widget _buildScaffoldStack(
SafeArea(child: _buildBodyWrapper(viewModel)); {required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Stack(children: [
_buildScaffold(context: context, viewModel: viewModel),
_buildState(viewModel)
]);
Widget _buildBodyWrapper(ProfileDetailViewModel viewModel) => Padding( Widget _buildScaffold(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
SafeArea(
child: _buildBodyWrapper(context: context, viewModel: viewModel));
Widget _buildBodyWrapper(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel), child: _buildBody(context: context, viewModel: viewModel),
); );
Widget _buildBody(ProfileDetailViewModel viewModel) => Column( Widget _buildBody(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildBodyChildren(viewModel), children: _buildBodyChildren(context: context, viewModel: viewModel),
); );
List<Widget> _buildBodyChildren(ProfileDetailViewModel viewModel) => [ List<Widget> _buildBodyChildren(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
[
verticalSpaceMedium, verticalSpaceMedium,
_buildAppbar(viewModel), _buildAppbar(viewModel),
verticalSpaceSmall, verticalSpaceSmall,
_buildColumnWrapper(viewModel) _buildColumnWrapper(context: context, viewModel: viewModel)
]; ];
Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar( Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar(
@ -85,23 +152,33 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
onTap: viewModel.pop, onTap: viewModel.pop,
); );
Widget _buildColumnWrapper(ProfileDetailViewModel viewModel) => Widget _buildColumnWrapper(
Expanded(child: _buildBodyColumn(viewModel)); {required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Expanded(child: _buildBodyColumn(context: context, viewModel: viewModel));
Widget _buildBodyColumn(ProfileDetailViewModel viewModel) => Widget _buildBodyColumn(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
SingleChildScrollView( SingleChildScrollView(
child: _buildColumn(viewModel), child: _buildColumn(context: context, viewModel: viewModel),
); );
Widget _buildColumn(ProfileDetailViewModel viewModel) => Column( Widget _buildColumn(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _buildColumnChildren(viewModel), children: _buildColumnChildren(context: context, viewModel: viewModel),
); );
List<Widget> _buildColumnChildren(ProfileDetailViewModel viewModel) => [ List<Widget> _buildColumnChildren(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
[
verticalSpaceMedium, verticalSpaceMedium,
_buildProfileImage(), _buildProfileImageWrapper(context: context, viewModel: viewModel),
verticalSpaceMedium, verticalSpaceMedium,
_buildNameFormSection(viewModel), _buildNameFormSection(viewModel),
verticalSpaceMedium, verticalSpaceMedium,
@ -120,8 +197,30 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
_buildLowerColumn(viewModel) _buildLowerColumn(viewModel)
]; ];
Widget _buildProfileImage() => Widget _buildProfileImageWrapper(
const Align(alignment: Alignment.center, child: ProfileImage()); {required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
Align(
alignment: Alignment.center,
child: _buildProfileImage(context: context, viewModel: viewModel));
Widget _buildProfileImage(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
ProfileImage(
profileImage: viewModel.user?.profilePicture,
loading: viewModel.busy(StateObjects.profileImage) ? true : false,
onTap: () async =>
await _showImagePicker(context: context, viewModel: viewModel),
);
Widget _buildImagePicker(
{required BuildContext context,
required ProfileDetailViewModel viewModel}) =>
ImagePickerOption(
onCameraTap: () async => await viewModel.openCamera(),
onGalleryTap: () async => await viewModel.openGallery(),
);
Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row( Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -368,6 +467,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) => Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) =>
TextFormField( TextFormField(
maxLength: 12, maxLength: 12,
enabled: false,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
controller: phoneNumberController, controller: phoneNumberController,
onTap: viewModel.setPhoneNumberFocus, onTap: viewModel.setPhoneNumberFocus,
@ -413,6 +513,7 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildEmailFormField(ProfileDetailViewModel viewModel) => Widget _buildEmailFormField(ProfileDetailViewModel viewModel) =>
TextFormField( TextFormField(
enabled: false,
controller: emailController, controller: emailController,
onTap: viewModel.setPhoneNumberFocus, onTap: viewModel.setPhoneNumberFocus,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
@ -471,10 +572,10 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) => Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker( CustomDropdownPicker(
onChanged: (value) {},
hint: 'Select country', hint: 'Select country',
selectedItem: 'Ethiopia', selectedItem: viewModel.selectedCountry,
items: (value, props) => viewModel.getCountries(), items: (value, props) => viewModel.getCountries(),
onChanged: (value) => viewModel.setSelectedCountry(value ?? 'Ethiopia'),
); );
Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) => Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) =>
@ -504,9 +605,11 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) => Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker( CustomDropdownPicker(
hint: 'Select region', hint: 'Select region',
onChanged: (value) {}, selectedItem: viewModel.selectedRegion,
selectedItem: 'Addis Ababa', items: (value, props) =>
items: (value, props) => viewModel.getRegions('Addis Ababa'), viewModel.getRegions(viewModel.selectedCountry),
onChanged: (value) =>
viewModel.setSelectedRegion(value ?? 'Addis Ababa'),
); );
Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) => Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) =>
@ -522,7 +625,13 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
[ [
_buildOccupationDropdownLabel(), _buildOccupationDropdownLabel(),
verticalSpaceSmall, verticalSpaceSmall,
_buildOccupationDropdown(viewModel) _buildOccupationFormField(viewModel),
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
verticalSpaceTiny,
if (viewModel.hasOccupationValidationMessage &&
viewModel.focusOccupation)
_buildOccupationValidatorWrapper(viewModel)
]; ];
Widget _buildOccupationDropdownLabel() => CustomFormLabel( Widget _buildOccupationDropdownLabel() => CustomFormLabel(
@ -530,14 +639,29 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
style: style16DG600, style: style16DG600,
); );
Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) => Widget _buildOccupationFormField(ProfileDetailViewModel viewModel) =>
CustomDropdownPicker( TextFormField(
hint: 'Select occupation', controller: occupationController,
onChanged: (value) {}, onTap: viewModel.setOccupationFocus,
selectedItem: 'Student', decoration: inputDecoration(
items: (value, props) => viewModel.getOccupations('Student'), hint: 'Enter Your Occupation',
focus: viewModel.focusOccupation,
filled: occupationController.text.isNotEmpty),
); );
Widget _buildOccupationValidatorWrapper(ProfileDetailViewModel viewModel) =>
viewModel.hasOccupationValidationMessage
? _buildOccupationValidator(viewModel)
: Container();
Widget _buildOccupationValidator(ProfileDetailViewModel viewModel) => Text(
viewModel.occupationValidationMessage!,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w700,
),
);
Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column( Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: _buildLowerColumnChildren(viewModel), children: _buildLowerColumnChildren(viewModel),
@ -545,17 +669,18 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
List<Widget> _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [ List<Widget> _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [
_buildSaveButton(viewModel), _buildSaveButton(viewModel),
verticalSpaceSmall, verticalSpaceMedium,
_buildCancelButtonWrapper(viewModel) _buildCancelButtonWrapper(viewModel)
]; ];
Widget _buildSaveButton(ProfileDetailViewModel viewModel) => Widget _buildSaveButton(ProfileDetailViewModel viewModel) =>
const CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
borderRadius: 12, borderRadius: 12,
text: 'Save Changes', text: 'Save Changes',
foregroundColor: kcWhite, foregroundColor: kcWhite,
backgroundColor: kcPrimaryColor, backgroundColor: kcPrimaryColor,
onTap: () async => await _update(viewModel),
); );
Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding( Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding(
@ -564,12 +689,18 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
); );
Widget _buildCancelButton(ProfileDetailViewModel viewModel) => Widget _buildCancelButton(ProfileDetailViewModel viewModel) =>
const CustomElevatedButton( CustomElevatedButton(
height: 55, height: 55,
text: 'Cancel', text: 'Cancel',
borderRadius: 12, borderRadius: 12,
borderColor: kcPrimaryColor, onTap: viewModel.pop,
backgroundColor: kcWhite, backgroundColor: kcWhite,
borderColor: kcPrimaryColor,
foregroundColor: kcPrimaryColor, foregroundColor: kcPrimaryColor,
); );
Widget _buildState(ProfileDetailViewModel viewModel) =>
viewModel.busy(StateObjects.profileUpdate)
? const PageLoadingIndicator()
: Container();
} }

View File

@ -16,6 +16,7 @@ const String EmailValueKey = 'email';
const String PhoneNumberValueKey = 'phoneNumber'; const String PhoneNumberValueKey = 'phoneNumber';
const String LastNameValueKey = 'lastName'; const String LastNameValueKey = 'lastName';
const String FirstNameValueKey = 'firstName'; const String FirstNameValueKey = 'firstName';
const String OccupationValueKey = 'occupation';
final Map<String, TextEditingController> final Map<String, TextEditingController>
_ProfileDetailViewTextEditingControllers = {}; _ProfileDetailViewTextEditingControllers = {};
@ -28,6 +29,7 @@ final Map<String, String? Function(String?)?>
PhoneNumberValueKey: FormValidator.validatePhoneNumber, PhoneNumberValueKey: FormValidator.validatePhoneNumber,
LastNameValueKey: FormValidator.validateForm, LastNameValueKey: FormValidator.validateForm,
FirstNameValueKey: FormValidator.validateForm, FirstNameValueKey: FormValidator.validateForm,
OccupationValueKey: FormValidator.validateForm,
}; };
mixin $ProfileDetailView { mixin $ProfileDetailView {
@ -39,11 +41,14 @@ mixin $ProfileDetailView {
_getFormTextEditingController(LastNameValueKey); _getFormTextEditingController(LastNameValueKey);
TextEditingController get firstNameController => TextEditingController get firstNameController =>
_getFormTextEditingController(FirstNameValueKey); _getFormTextEditingController(FirstNameValueKey);
TextEditingController get occupationController =>
_getFormTextEditingController(OccupationValueKey);
FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey);
FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey);
FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey); FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey);
FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey); FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey);
FocusNode get occupationFocusNode => _getFormFocusNode(OccupationValueKey);
TextEditingController _getFormTextEditingController( TextEditingController _getFormTextEditingController(
String key, { String key, {
@ -73,6 +78,7 @@ mixin $ProfileDetailView {
phoneNumberController.addListener(() => _updateFormData(model)); phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model)); lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model)); firstNameController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation); _updateFormData(model, forceValidate: _autoTextFieldValidation);
} }
@ -88,6 +94,7 @@ mixin $ProfileDetailView {
phoneNumberController.addListener(() => _updateFormData(model)); phoneNumberController.addListener(() => _updateFormData(model));
lastNameController.addListener(() => _updateFormData(model)); lastNameController.addListener(() => _updateFormData(model));
firstNameController.addListener(() => _updateFormData(model)); firstNameController.addListener(() => _updateFormData(model));
occupationController.addListener(() => _updateFormData(model));
_updateFormData(model, forceValidate: _autoTextFieldValidation); _updateFormData(model, forceValidate: _autoTextFieldValidation);
} }
@ -101,6 +108,7 @@ mixin $ProfileDetailView {
PhoneNumberValueKey: phoneNumberController.text, PhoneNumberValueKey: phoneNumberController.text,
LastNameValueKey: lastNameController.text, LastNameValueKey: lastNameController.text,
FirstNameValueKey: firstNameController.text, FirstNameValueKey: firstNameController.text,
OccupationValueKey: occupationController.text,
}), }),
); );
@ -147,6 +155,8 @@ extension ValueProperties on FormStateHelper {
this.formValueMap[PhoneNumberValueKey] as String?; this.formValueMap[PhoneNumberValueKey] as String?;
String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?; String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?;
String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?; String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?;
String? get occupationValue =>
this.formValueMap[OccupationValueKey] as String?;
set emailValue(String? value) { set emailValue(String? value) {
this.setData( this.setData(
@ -195,6 +205,18 @@ extension ValueProperties on FormStateHelper {
} }
} }
set occupationValue(String? value) {
this.setData(
this.formValueMap..addAll({OccupationValueKey: value}),
);
if (_ProfileDetailViewTextEditingControllers.containsKey(
OccupationValueKey)) {
_ProfileDetailViewTextEditingControllers[OccupationValueKey]?.text =
value ?? '';
}
}
bool get hasEmail => bool get hasEmail =>
this.formValueMap.containsKey(EmailValueKey) && this.formValueMap.containsKey(EmailValueKey) &&
(emailValue?.isNotEmpty ?? false); (emailValue?.isNotEmpty ?? false);
@ -207,6 +229,9 @@ extension ValueProperties on FormStateHelper {
bool get hasFirstName => bool get hasFirstName =>
this.formValueMap.containsKey(FirstNameValueKey) && this.formValueMap.containsKey(FirstNameValueKey) &&
(firstNameValue?.isNotEmpty ?? false); (firstNameValue?.isNotEmpty ?? false);
bool get hasOccupation =>
this.formValueMap.containsKey(OccupationValueKey) &&
(occupationValue?.isNotEmpty ?? false);
bool get hasEmailValidationMessage => bool get hasEmailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false;
@ -216,6 +241,8 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false; this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false;
bool get hasFirstNameValidationMessage => bool get hasFirstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false; this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false;
bool get hasOccupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey]?.isNotEmpty ?? false;
String? get emailValidationMessage => String? get emailValidationMessage =>
this.fieldsValidationMessages[EmailValueKey]; this.fieldsValidationMessages[EmailValueKey];
@ -225,6 +252,8 @@ extension ValueProperties on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey]; this.fieldsValidationMessages[LastNameValueKey];
String? get firstNameValidationMessage => String? get firstNameValidationMessage =>
this.fieldsValidationMessages[FirstNameValueKey]; this.fieldsValidationMessages[FirstNameValueKey];
String? get occupationValidationMessage =>
this.fieldsValidationMessages[OccupationValueKey];
} }
extension Methods on FormStateHelper { extension Methods on FormStateHelper {
@ -236,6 +265,8 @@ extension Methods on FormStateHelper {
this.fieldsValidationMessages[LastNameValueKey] = validationMessage; this.fieldsValidationMessages[LastNameValueKey] = validationMessage;
setFirstNameValidationMessage(String? validationMessage) => setFirstNameValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[FirstNameValueKey] = validationMessage; this.fieldsValidationMessages[FirstNameValueKey] = validationMessage;
setOccupationValidationMessage(String? validationMessage) =>
this.fieldsValidationMessages[OccupationValueKey] = validationMessage;
/// Clears text input fields on the Form /// Clears text input fields on the Form
void clearForm() { void clearForm() {
@ -243,6 +274,7 @@ extension Methods on FormStateHelper {
phoneNumberValue = ''; phoneNumberValue = '';
lastNameValue = ''; lastNameValue = '';
firstNameValue = ''; firstNameValue = '';
occupationValue = '';
} }
/// Validates text input fields on the Form /// Validates text input fields on the Form
@ -252,6 +284,7 @@ extension Methods on FormStateHelper {
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey), LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey), FirstNameValueKey: getValidationMessage(FirstNameValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
}); });
} }
} }
@ -275,4 +308,5 @@ void updateValidationData(FormStateHelper model) =>
PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey),
LastNameValueKey: getValidationMessage(LastNameValueKey), LastNameValueKey: getValidationMessage(LastNameValueKey),
FirstNameValueKey: getValidationMessage(FirstNameValueKey), FirstNameValueKey: getValidationMessage(FirstNameValueKey),
OccupationValueKey: getValidationMessage(OccupationValueKey),
}); });

View File

@ -2,9 +2,36 @@ import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_services/stacked_services.dart';
import '../../../app/app.locator.dart'; import '../../../app/app.locator.dart';
import '../../../models/user_model.dart';
import '../../../services/api_service.dart';
import '../../../services/authentication_service.dart';
import '../../../services/image_picker_service.dart';
import '../../../services/status_checker_service.dart';
import '../../common/enmus.dart';
import '../../common/ui_helpers.dart';
class ProfileDetailViewModel extends ReactiveViewModel
with FormStateHelper
implements FormViewModel {
final _apiService = locator<ApiService>();
final _statusChecker = locator<StatusCheckerService>();
class ProfileDetailViewModel extends FormViewModel {
final _navigationService = locator<NavigationService>(); final _navigationService = locator<NavigationService>();
final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>();
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
// Current user
UserModel? get _user => _authenticationService.user;
UserModel? get user => _user;
// First name // First name
bool _focusFirstName = false; bool _focusFirstName = false;
@ -35,6 +62,26 @@ class ProfileDetailViewModel extends FormViewModel {
bool get focusEmail => _focusEmail; bool get focusEmail => _focusEmail;
// Country
String _selectedCountry = 'Ethiopia';
String get selectedCountry => _selectedCountry;
// Region
String _selectedRegion = 'Addis Ababa';
String get selectedRegion => _selectedRegion;
// Occupation
bool _focusOccupation = false;
bool get focusOccupation => _focusOccupation;
// User data
final Map<String, dynamic> _userData = {};
Map<String, dynamic> get userData => _userData;
// First name // First name
void setFirstNameFocus() { void setFirstNameFocus() {
_focusFirstName = true; _focusFirstName = true;
@ -72,15 +119,132 @@ class ProfileDetailViewModel extends FormViewModel {
} }
// Country // Country
Future<List<String>> getCountries() async => ['Ethiopia', 'Djibouti']; List<String> getCountries() => ['Ethiopia', 'Other'];
void setSelectedCountry(String value) {
_selectedCountry = value;
if (selectedCountry != 'Ethiopia') {
_selectedRegion = 'Other';
} else {
_selectedRegion = 'Addis Ababa';
}
rebuildUi();
}
// Region // Region
Future<List<String>> getRegions(String country) async => List<String> getRegions(String country) {
['Addis Ababa', 'Oromia']; if (country == 'Ethiopia') {
return [
'Afar',
'SNNPR',
'Amhara',
'Harari',
'Oromia',
'Sidama',
'Somali',
'Tigray',
'Gambela',
'Dire Dawa',
'Addis Ababa',
'Central Ethiopia',
'Benishangul-Gumuz',
'South West Ethiopia',
];
} else {
return ['Other'];
}
}
void setSelectedRegion(String value) {
_selectedRegion = value;
rebuildUi();
}
// Occupation // Occupation
Future<List<String>> getOccupations(String country) async => void setOccupationFocus() {
['Student', 'Worker']; _focusOccupation = true;
rebuildUi();
}
// User data
void addUserData(Map<String, dynamic> data) {
_userData.addAll(data);
}
void clearUserData() {
_userData.clear();
}
// Image picker
Future<void> openCamera() async =>
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
Future<void> _openCamera() async {
String? image = await _imagePickerService.camera();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
Future<void> openGallery() async =>
runBusyFuture(_openGallery(), busyObject: StateObjects.profileImage);
Future<void> _openGallery() async {
String? image = await _imagePickerService.gallery();
pop();
if (image != null) {
await updateProfilePicture(image);
await _authenticationService.saveProfileImage(image);
}
}
// Navigation
void pop() => _navigationService.back(); void pop() => _navigationService.back();
// Remote api call
// Get profile
Future<void> getProfile() async => await runBusyFuture(_getProfile());
Future<void> _getProfile() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.getProfileData(_user?.userId);
if (response['status'] == ResponseStatus.success) {
addUserData(response['data']);
}
}
}
// Update profile
Future<void> updateProfile() async => await runBusyFuture(_updateProfile(),
busyObject: StateObjects.profileUpdate);
Future<void> _updateProfile() async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> response =
await _apiService.completeProfile(_userData);
if (response['status'] == ResponseStatus.success) {
await _authenticationService.updateUserData(_userData);
pop();
showSuccessToast(response['message']);
} else {
showErrorToast(response['message']);
}
}
}
// Update profile picture
Future<void> updateProfilePicture(String image) async =>
await runBusyFuture(_updateProfilePicture(image));
Future<void> _updateProfilePicture(String image) async {
if (await _statusChecker.checkConnection()) {
Map<String, dynamic> data = {'profile_picture_url': image};
await _apiService.updateProfileImage(data: data, userId: _user?.userId);
}
}
} }

View File

@ -101,7 +101,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
title: viewModel.progresses[index]['title'], title: viewModel.progresses[index]['title'],
color: viewModel.progresses[index]['color'], color: viewModel.progresses[index]['color'],
status: viewModel.progresses[index]['status'], status: viewModel.progresses[index]['status'],
subTitle: viewModel.progresses[index]['subTitle'], subtitle: viewModel.progresses[index]['subtitle'],
isCompleted: viewModel.progresses[index]['isCompleted'], isCompleted: viewModel.progresses[index]['isCompleted'],
), ),
); );
@ -111,7 +111,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
required String title, required String title,
required String icon, required String icon,
required String status, required String status,
required String subTitle, required String subtitle,
required bool isCompleted, required bool isCompleted,
required ProgressViewModel viewModel}) => required ProgressViewModel viewModel}) =>
CourseLevelCard( CourseLevelCard(
@ -119,7 +119,7 @@ class ProgressView extends StackedView<ProgressViewModel> {
title: title, title: title,
color: color, color: color,
status: status, status: status,
subTitle: subTitle, subtitle: subtitle,
isCompleted: isCompleted, isCompleted: isCompleted,
onTap: viewModel.navigateToOngoingProgress, onTap: viewModel.navigateToOngoingProgress,
); );

View File

@ -14,24 +14,24 @@ class ProgressViewModel extends BaseViewModel {
'title': 'Beginner', 'title': 'Beginner',
'isCompleted': true, 'isCompleted': true,
'status': 'Completed', 'status': 'Completed',
'icon': 'assets/icons/b1.svg', 'icon': 'assets/icons/b_1.svg',
'subTitle': 'Youve mastered everyday English basics!', 'subtitle': 'Youve mastered everyday English basics!',
}, },
{ {
'title': 'Elementary', 'title': 'Elementary',
'isCompleted': false, 'isCompleted': false,
'status': 'In Progress', 'status': 'In Progress',
'color': kcPrimaryColor, 'color': kcPrimaryColor,
'icon': 'assets/icons/b1.svg', 'icon': 'assets/icons/b_1.svg',
'subTitle': 'Continue improving your conversations and fluency.', 'subtitle': 'Continue improving your conversations and fluency.',
}, },
{ {
'title': 'Beginner', 'title': 'Beginner',
'isCompleted': true, 'isCompleted': true,
'status': 'In Progress', 'status': 'In Progress',
'color': kcPrimaryColor, 'color': kcPrimaryColor,
'icon': 'assets/icons/b1.svg', 'icon': 'assets/icons/b_1.svg',
'subTitle': 'Youve mastered everyday English basics!', 'subtitle': 'Youve mastered everyday English basics!',
}, },
]; ];

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart'; import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart'; import 'package:stacked/stacked_annotations.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart'; import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart'; import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart';
import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart'; import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart';
@ -24,8 +25,46 @@ import 'register_view.form.dart';
class RegisterView extends StackedView<RegisterViewModel> with $RegisterView { class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
const RegisterView({Key? key}) : super(key: key); const RegisterView({Key? key}) : super(key: key);
void _initClearData() {
otpController.clear();
emailController.clear();
passwordController.clear();
phoneNumberController.clear();
confirmPasswordController.clear();
}
void _inAppPop(RegisterViewModel viewModel) {
print('OnPop');
print(viewModel.currentPage);
_clearDataOnNavigation(viewModel);
viewModel.goBack();
}
void _clearDataOnNavigation(RegisterViewModel viewModel) {
if (viewModel.currentPage == 0) {
emailController.clear();
viewModel.resetRegisterWithEmailScreen();
} else if (viewModel.currentPage == 2) {
passwordController.clear();
confirmPasswordController.clear();
viewModel.resetCreatePasswordScreen();
} else if (viewModel.currentPage == 3) {
otpController.clear();
viewModel.resetRegistrationOtpScreen();
}
}
void _pop({required bool value, required RegisterViewModel viewModel}) {
{
if (!value) return;
_clearDataOnNavigation(viewModel);
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack());
}
}
@override @override
void onViewModelReady(RegisterViewModel viewModel) { void onViewModelReady(RegisterViewModel viewModel) {
_initClearData();
syncFormWithViewModel(viewModel); syncFormWithViewModel(viewModel);
super.onViewModelReady(viewModel); super.onViewModelReady(viewModel);
} }
@ -44,44 +83,14 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope( Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope(
canPop: false, canPop: false,
onPopInvokedWithResult: (value, data) { onPopInvokedWithResult: (value, data) =>
if (value) return; _pop(value: value, viewModel: viewModel),
WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); child: _buildBody(viewModel));
},
child: _buildScaffoldWrapper(viewModel));
Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffoldStack(viewModel),
);
Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack(
children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]);
Widget _buildScaffold(RegisterViewModel viewModel) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildScaffoldChildren(viewModel),
);
List<Widget> _buildScaffoldChildren(RegisterViewModel viewModel) =>
[_buildAppBar(viewModel), _buildExpandedBody(viewModel)];
Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar(
showBackButton: true,
onPop: viewModel.goBack,
showLanguageSelection: true,
);
Widget _buildExpandedBody(RegisterViewModel viewModel) =>
Expanded(child: _buildBodyWrapper(viewModel));
Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildBody(viewModel),
);
Widget _buildBody(RegisterViewModel viewModel) => Widget _buildBody(RegisterViewModel viewModel) =>
IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); IndexedStack(index: viewModel.currentPage, children: _buildScreens());
List<Widget> _buildScreens() => [ List<Widget> _buildScreens() => [
_buildRegisterWithEmailScreen(), _buildRegisterWithEmailScreen(),
@ -106,6 +115,4 @@ class RegisterView extends StackedView<RegisterViewModel> with $RegisterView {
passwordController: passwordController, passwordController: passwordController,
confirmPasswordController: confirmPasswordController); confirmPasswordController: confirmPasswordController);
Widget _buildBusyRegistration(RegisterViewModel viewModel) =>
viewModel.isBusy ? const PageLoadingIndicator() : Container();
} }

Some files were not shown because too many files have changed in this diff Show More