feat(auth): Add google sign-in option
This commit is contained in:
parent
4ef204f31b
commit
8110e25cb9
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
android/app/google-services.json
Normal file
51
android/app/google-services.json
Normal 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"
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
<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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
5
assets/icons/a_1.svg
Normal 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
5
assets/icons/a_2.svg
Normal 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 |
|
|
@ -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
5
assets/icons/b_1.svg
Normal 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
5
assets/icons/b_2.svg
Normal 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 |
1
firebase.json
Normal file
1
firebase.json
Normal 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"}}}}}}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ 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/ui/views/failure/failure_view.dart';
|
||||||
import 'package:yimaru_app/services/permission_handler_service.dart';
|
import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||||
|
import 'package:yimaru_app/services/google_auth_service.dart';
|
||||||
|
import 'package:yimaru_app/services/image_downloader_service.dart';
|
||||||
// @stacked-import
|
// @stacked-import
|
||||||
|
|
||||||
@StackedApp(
|
@StackedApp(
|
||||||
|
|
@ -74,6 +76,8 @@ import 'package:yimaru_app/services/image_picker_service.dart';
|
||||||
LazySingleton(classType: StatusCheckerService),
|
LazySingleton(classType: StatusCheckerService),
|
||||||
LazySingleton(classType: PermissionHandlerService),
|
LazySingleton(classType: PermissionHandlerService),
|
||||||
LazySingleton(classType: ImagePickerService),
|
LazySingleton(classType: ImagePickerService),
|
||||||
|
LazySingleton(classType: GoogleAuthService),
|
||||||
|
LazySingleton(classType: ImageDownloaderService),
|
||||||
// @stacked-service
|
// @stacked-service
|
||||||
],
|
],
|
||||||
bottomsheets: [
|
bottomsheets: [
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ 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/image_picker_service.dart';
|
||||||
import '../services/permission_handler_service.dart';
|
import '../services/permission_handler_service.dart';
|
||||||
import '../services/secure_storage_service.dart';
|
import '../services/secure_storage_service.dart';
|
||||||
|
|
@ -40,4 +42,6 @@ Future<void> setupLocator({
|
||||||
locator.registerLazySingleton(() => StatusCheckerService());
|
locator.registerLazySingleton(() => StatusCheckerService());
|
||||||
locator.registerLazySingleton(() => PermissionHandlerService());
|
locator.registerLazySingleton(() => PermissionHandlerService());
|
||||||
locator.registerLazySingleton(() => ImagePickerService());
|
locator.registerLazySingleton(() => ImagePickerService());
|
||||||
|
locator.registerLazySingleton(() => GoogleAuthService());
|
||||||
|
locator.registerLazySingleton(() => ImageDownloaderService());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
lib/firebase_options.dart
Normal file
70
lib/firebase_options.dart
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
|
|
@ -4,14 +4,27 @@ part 'user_model.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class UserModel {
|
class UserModel {
|
||||||
final String? firstName;
|
final String? email;
|
||||||
|
|
||||||
|
final String? gender;
|
||||||
|
|
||||||
|
final String? region;
|
||||||
|
|
||||||
|
final String? country;
|
||||||
|
|
||||||
|
final String? occupation;
|
||||||
|
|
||||||
@JsonKey(name: 'user_id')
|
@JsonKey(name: 'user_id')
|
||||||
final int? userId;
|
final int? userId;
|
||||||
|
|
||||||
final String? profileImage;
|
@JsonKey(name: 'last_name')
|
||||||
|
final String? lastName;
|
||||||
|
|
||||||
final bool? profileCompleted;
|
@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;
|
||||||
|
|
@ -19,12 +32,27 @@ class UserModel {
|
||||||
@JsonKey(name: 'refresh_token')
|
@JsonKey(name: 'refresh_token')
|
||||||
final String? refreshToken;
|
final String? refreshToken;
|
||||||
|
|
||||||
|
@JsonKey(name: 'profile_completed')
|
||||||
|
final bool? profileCompleted;
|
||||||
|
|
||||||
|
@JsonKey(name: 'profile_picture_url')
|
||||||
|
final String? profilePicture;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const UserModel({
|
const UserModel({
|
||||||
|
this.email,
|
||||||
|
this.region,
|
||||||
|
this.gender,
|
||||||
this.userId,
|
this.userId,
|
||||||
|
this.country,
|
||||||
|
this.lastName,
|
||||||
|
this.birthday,
|
||||||
this.firstName,
|
this.firstName,
|
||||||
|
this.occupation,
|
||||||
this.accessToken,
|
this.accessToken,
|
||||||
this.profileImage,
|
|
||||||
this.refreshToken,
|
this.refreshToken,
|
||||||
|
this.profilePicture,
|
||||||
this.profileCompleted,
|
this.profileCompleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,33 @@ 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(),
|
||||||
firstName: json['firstName'] as String?,
|
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?,
|
||||||
profileImage: json['profileImage'] as String?,
|
|
||||||
refreshToken: json['refresh_token'] as String?,
|
refreshToken: json['refresh_token'] as String?,
|
||||||
profileCompleted: json['profileCompleted'] as bool?,
|
profilePicture: json['profile_picture_url'] as String?,
|
||||||
|
profileCompleted: json['profile_completed'] as bool?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
|
Map<String, dynamic> _$UserModelToJson(UserModel instance) => <String, dynamic>{
|
||||||
'firstName': instance.firstName,
|
|
||||||
'user_id': instance.userId,
|
'user_id': instance.userId,
|
||||||
'profileImage': instance.profileImage,
|
'last_name': instance.lastName,
|
||||||
'profileCompleted': instance.profileCompleted,
|
'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,
|
||||||
|
'email': instance.email,
|
||||||
|
'gender': instance.gender,
|
||||||
|
'region': instance.region,
|
||||||
|
'country': instance.country,
|
||||||
|
'occupation': instance.occupation,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async {
|
Future<Map<String, dynamic>> register(Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.post(
|
Response response = await _service.dio.post(
|
||||||
'$baseUrl/$kUserUrl/$kRegisterUrl',
|
'$kBaseUrl/$kUserUrl/$kRegisterUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -38,10 +38,38 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login
|
// 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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']}'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
'message': e.toString(),
|
||||||
|
'status': ResponseStatus.failure,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google login
|
||||||
|
Future<Map<String, dynamic>> googleLogin(Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
Response response = await _service.dio.post(
|
||||||
|
'$kBaseUrl/$kGoogleLoginUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -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/$kUserUrl/$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 {
|
||||||
|
|
@ -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/$kUserUrl/$kResendOtpUrl',
|
'$kBaseUrl/$kUserUrl/$kResendOtpUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -123,7 +151,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/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
|
'$kBaseUrl/$kUserUrl/${user?.userId}/$kProfileStatusUrl',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
|
|
@ -150,14 +178,14 @@ class ApiService {
|
||||||
Future<Map<String, dynamic>> getProfileData(int? userId) async {
|
Future<Map<String, dynamic>> getProfileData(int? userId) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.get(
|
Response response = await _service.dio.get(
|
||||||
'$baseUrl/$kUserUrl/$kGetUserUrl/$userId',
|
'$kBaseUrl/$kUserUrl/$kGetUserUrl/$userId',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return {
|
return {
|
||||||
'data': response.data['data'],
|
|
||||||
'status': ResponseStatus.success,
|
'status': ResponseStatus.success,
|
||||||
'message': 'Profile fetched successfully'
|
'message': 'Profile fetched successfully',
|
||||||
|
'data': UserModel.fromJson(response.data['data']),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|
@ -173,12 +201,12 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profile
|
// Complete profile
|
||||||
Future<Map<String, dynamic>> updateProfile(
|
Future<Map<String, dynamic>> completeProfile(
|
||||||
{required UserModel? user, required Map<String, dynamic> data}) async {
|
Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
Response response = await _service.dio.put(
|
Response response = await _service.dio.put(
|
||||||
'$baseUrl/$kUserUrl',
|
'$kBaseUrl/$kUserUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -201,13 +229,156 @@ class ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
'message': e.toString(),
|
||||||
|
'status': ResponseStatus.failure,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// // Update profile
|
||||||
|
// Future<Map<String, dynamic>> updateProfile(
|
||||||
|
// Map<String, dynamic> data) async {
|
||||||
|
// try {
|
||||||
|
// late FormData formData;
|
||||||
|
//
|
||||||
|
// if (data['profile_picture_url']
|
||||||
|
// .toString()
|
||||||
|
// .contains('com.ke.wede.customer.app/')) {
|
||||||
|
// formData = FormData.fromMap({
|
||||||
|
// 'gender': data['gender'],
|
||||||
|
// 'region': data['region'],
|
||||||
|
// 'country': data['country'],
|
||||||
|
// 'last_name': data['last_name'],
|
||||||
|
// 'nick_name': data['nick_name'],
|
||||||
|
// 'birth_day': data['birth_day'],
|
||||||
|
// 'age_group': data['age_group'],
|
||||||
|
// 'occupation': data['occupation'],
|
||||||
|
// 'first_name': data['first_name'],
|
||||||
|
// 'learning_goal': data['learning_goal'],
|
||||||
|
// 'language_goal': data['language_goal'],
|
||||||
|
// 'education_level': data['education_level'],
|
||||||
|
// 'favoutite_topic': data['favoutite_topic'],
|
||||||
|
// 'knowledge_level': data['knowledge_level'],
|
||||||
|
// 'profile_completed': data['profile_completed'],
|
||||||
|
// 'preferred_language': data['preferred_language'],
|
||||||
|
// 'language_challange': data['language_challange'],
|
||||||
|
// 'profile_picture_url': 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({
|
||||||
|
// 'gender': data['gender'],
|
||||||
|
// 'region': data['region'],
|
||||||
|
// 'country': data['country'],
|
||||||
|
// 'last_name': data['last_name'],
|
||||||
|
// 'nick_name': data['nick_name'],
|
||||||
|
// 'birth_day': data['birth_day'],
|
||||||
|
// 'age_group': data['age_group'],
|
||||||
|
// 'occupation': data['occupation'],
|
||||||
|
// 'first_name': data['first_name'],
|
||||||
|
// 'learning_goal': data['learning_goal'],
|
||||||
|
// 'language_goal': data['language_goal'],
|
||||||
|
// 'education_level': data['education_level'],
|
||||||
|
// 'favoutite_topic': data['favoutite_topic'],
|
||||||
|
// 'knowledge_level': data['knowledge_level'],
|
||||||
|
// 'profile_completed': data['profile_completed'],
|
||||||
|
// 'preferred_language': data['preferred_language'],
|
||||||
|
// 'language_challange': data['language_challange'],
|
||||||
|
// 'profile_picture_url': 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.put(
|
||||||
|
// '$baseUrl/$kUserUrl',
|
||||||
|
// data: formData,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// if (response.statusCode == 200) {
|
||||||
|
// return {
|
||||||
|
// 'status': ResponseStatus.success,
|
||||||
|
// 'message': 'Profile updated successfully'
|
||||||
|
// };
|
||||||
|
// } else {
|
||||||
|
// return {
|
||||||
|
// 'status': ResponseStatus.failure,
|
||||||
|
// 'message': 'Unknown Error Occurred'
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// return {
|
||||||
|
// 'message': e.toString(),
|
||||||
|
// 'status': ResponseStatus.failure,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
// Assessments
|
// Assessments
|
||||||
Future<List<Assessment>> getAssessments() async {
|
Future<List<Assessment>> getAssessments() async {
|
||||||
try {
|
try {
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -40,23 +40,28 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
Future<void> saveUserName(Map<String, dynamic> data) async {
|
Future<void> saveUserName(Map<String, dynamic> data) async {
|
||||||
await _secureService.setString('firstName', data['firstName']);
|
await _secureService.setString('firstName', data['firstName']);
|
||||||
_user = UserModel(
|
_user = UserModel(
|
||||||
|
email: _user?.email,
|
||||||
|
gender: _user?.gender,
|
||||||
|
region: _user?.region,
|
||||||
userId: _user?.userId,
|
userId: _user?.userId,
|
||||||
|
country: _user?.country,
|
||||||
|
lastName: _user?.lastName,
|
||||||
|
birthday: _user?.birthday,
|
||||||
|
occupation: _user?.occupation,
|
||||||
accessToken: _user?.accessToken,
|
accessToken: _user?.accessToken,
|
||||||
refreshToken: _user?.refreshToken,
|
refreshToken: _user?.refreshToken,
|
||||||
|
profilePicture: _user?.profilePicture,
|
||||||
profileCompleted: _user?.profileCompleted,
|
profileCompleted: _user?.profileCompleted,
|
||||||
firstName: await _secureService.getString('firstName'),
|
firstName: await _secureService.getString('firstName'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveBasicUserData(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(
|
_user = UserModel(
|
||||||
firstName: _user?.firstName,
|
|
||||||
profileImage: _user?.profileImage,
|
|
||||||
profileCompleted: _user?.profileCompleted,
|
|
||||||
userId: await _secureService.getInt('userId'),
|
userId: await _secureService.getInt('userId'),
|
||||||
accessToken: await _secureService.getString('accessToken'),
|
accessToken: await _secureService.getString('accessToken'),
|
||||||
refreshToken: await _secureService.getString('refreshToken'),
|
refreshToken: await _secureService.getString('refreshToken'),
|
||||||
|
|
@ -67,39 +72,101 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
await _secureService.setBool('profileCompleted', value);
|
await _secureService.setBool('profileCompleted', value);
|
||||||
|
|
||||||
_user = UserModel(
|
_user = UserModel(
|
||||||
|
email: _user?.email,
|
||||||
|
gender: _user?.gender,
|
||||||
|
region: _user?.region,
|
||||||
userId: _user?.userId,
|
userId: _user?.userId,
|
||||||
|
country: _user?.country,
|
||||||
|
lastName: _user?.lastName,
|
||||||
|
birthday: _user?.birthday,
|
||||||
firstName: _user?.firstName,
|
firstName: _user?.firstName,
|
||||||
|
occupation: _user?.occupation,
|
||||||
accessToken: _user?.accessToken,
|
accessToken: _user?.accessToken,
|
||||||
refreshToken: _user?.refreshToken,
|
refreshToken: _user?.refreshToken,
|
||||||
profileImage: _user?.profileImage,
|
profilePicture: _user?.profilePicture,
|
||||||
profileCompleted: await _secureService.getBool('profileCompleted'));
|
profileCompleted: await _secureService.getBool('profileCompleted'));
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveProfileImage(String image) async {
|
Future<void> saveProfileImage(String image) async {
|
||||||
await _secureService.setString('profileImage', image);
|
await _secureService.setString('profileImage', image);
|
||||||
_user = UserModel(
|
_user = UserModel(
|
||||||
|
email: _user?.email,
|
||||||
|
gender: _user?.gender,
|
||||||
|
region: _user?.region,
|
||||||
userId: _user?.userId,
|
userId: _user?.userId,
|
||||||
|
country: _user?.country,
|
||||||
|
lastName: _user?.lastName,
|
||||||
|
birthday: _user?.birthday,
|
||||||
firstName: _user?.firstName,
|
firstName: _user?.firstName,
|
||||||
|
occupation: _user?.occupation,
|
||||||
accessToken: _user?.accessToken,
|
accessToken: _user?.accessToken,
|
||||||
refreshToken: _user?.refreshToken,
|
refreshToken: _user?.refreshToken,
|
||||||
profileCompleted: _user?.profileCompleted,
|
profileCompleted: _user?.profileCompleted,
|
||||||
profileImage: await _secureService.getString('profileImage'),
|
profilePicture: await _secureService.getString('profileImage'),
|
||||||
);
|
);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveFullName(Map<String, dynamic> data) async {
|
Future<void> saveUserData(
|
||||||
await _secureService.setBool('profileCompleted', true);
|
{required String image, required UserModel data}) async {
|
||||||
await _secureService.setString('firstName', data['firstName']);
|
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(
|
_user = UserModel(
|
||||||
|
email: data.email,
|
||||||
|
gender: data.gender,
|
||||||
|
region: data.region,
|
||||||
|
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,
|
userId: _user?.userId,
|
||||||
accessToken: _user?.accessToken,
|
accessToken: _user?.accessToken,
|
||||||
refreshToken: _user?.refreshToken,
|
refreshToken: _user?.refreshToken,
|
||||||
profileImage: _user?.profileImage,
|
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'),
|
firstName: await _secureService.getString('firstName'),
|
||||||
profileCompleted: await _secureService.getBool('profileCompleted'),
|
occupation: await _secureService.getString('occupation'),
|
||||||
);
|
);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isFirstTimeInstall() async =>
|
Future<bool> isFirstTimeInstall() async =>
|
||||||
|
|
@ -112,10 +179,17 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
Future<UserModel?> getUser() async {
|
Future<UserModel?> getUser() async {
|
||||||
_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'),
|
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'),
|
||||||
profileImage: await _secureService.getString('profileImage'),
|
profilePicture: await _secureService.getString('profileImage'),
|
||||||
profileCompleted: await _secureService.getBool('profileCompleted'),
|
profileCompleted: await _secureService.getBool('profileCompleted'),
|
||||||
);
|
);
|
||||||
return _user;
|
return _user;
|
||||||
|
|
@ -123,6 +197,7 @@ class AuthenticationService with ListenableServiceMixin {
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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➡️➡️➡️➡️');
|
||||||
|
|
@ -131,13 +132,13 @@ class DioService {
|
||||||
|
|
||||||
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
|
||||||
};
|
};
|
||||||
final response = await _refreshDio.post(
|
final response = await _refreshDio.post(
|
||||||
'$baseUrl/$kRefreshTokenUrl',
|
'$kBaseUrl/$kRefreshTokenUrl',
|
||||||
data: data,
|
data: data,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -148,9 +149,8 @@ class DioService {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Token refresh exception ${e.toString()}');
|
await _authenticationService.logOut();
|
||||||
// await _authenticationService.logOut();
|
await _navigationService.replaceWithLoginView();
|
||||||
// await _navigationService.replaceWithLoginView();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
lib/services/google_auth_service.dart
Normal file
21
lib/services/google_auth_service.dart
Normal 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?> googleSignIn() 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
lib/services/image_downloader_service.dart
Normal file
33
lib/services/image_downloader_service.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
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 {
|
||||||
|
final Directory appDir = await getApplicationDocumentsDirectory();
|
||||||
|
|
||||||
|
late File image;
|
||||||
|
|
||||||
|
final Response profileImageResponse = await _service.dio.get(
|
||||||
|
'$kBaseUrl$networkImage',
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
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 kGetUserUrl = 'single';
|
String kGetUserUrl = 'single';
|
||||||
|
|
@ -11,10 +11,17 @@ String kVerifyOtpUrl = 'verify-otp';
|
||||||
|
|
||||||
String kResendOtpUrl = 'resend-otp';
|
String kResendOtpUrl = 'resend-otp';
|
||||||
|
|
||||||
|
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 kGoogleLoginUrl = 'api/v1/auth/google/android';
|
||||||
|
|
||||||
String kAssessmentsUrl = 'api/v1/assessment/questions';
|
String kAssessmentsUrl = 'api/v1/assessment/questions';
|
||||||
|
|
||||||
|
String kServerClientId =
|
||||||
|
'574860813475-n5o17gpprdqmhcml99tiqhafb17rob0r.apps.googleusercontent.com';
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,4 @@ enum ProgressStatuses { pending, started, completed }
|
||||||
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
enum ProficiencyLevels { a1, a2, b1, b2, none }
|
||||||
|
|
||||||
// State object
|
// State object
|
||||||
enum StateObjects{profileImage}
|
enum StateObjects { profileImage }
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,12 @@ TextStyle style14P600 = const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style25K600 = 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,
|
||||||
|
|
@ -221,6 +227,14 @@ TextStyle style16DG400 = const TextStyle(
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TextStyle style14LG400 = const TextStyle(
|
||||||
|
color: kcLightGrey,
|
||||||
|
);
|
||||||
|
|
||||||
|
TextStyle style14MG400 = const TextStyle(
|
||||||
|
color: kcMediumGrey,
|
||||||
|
);
|
||||||
|
|
||||||
TextStyle style14DG400 = const TextStyle(
|
TextStyle style14DG400 = const TextStyle(
|
||||||
color: kcDarkGrey,
|
color: kcDarkGrey,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,14 @@ 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.dialogs.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/app_colors.dart';
|
||||||
import '../../common/ui_helpers.dart';
|
import '../../common/ui_helpers.dart';
|
||||||
import '../home/home_view.dart';
|
import '../home/home_view.dart';
|
||||||
|
|
@ -19,8 +18,8 @@ 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 _dialogService = locator<DialogService>();
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
|
||||||
|
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
|
|
||||||
|
|
@ -35,7 +34,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;
|
||||||
|
|
@ -58,7 +56,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;
|
||||||
|
|
||||||
|
|
@ -106,7 +103,7 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
final correctCount = countCorrectAnswersUntil(16);
|
final correctCount = countCorrectAnswersUntil(16);
|
||||||
|
|
||||||
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};
|
||||||
}
|
}
|
||||||
|
|
@ -115,18 +112,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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -139,22 +138,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();
|
||||||
|
|
@ -169,30 +152,36 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
_userData.clear();
|
_userData.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete profile
|
// Dialog
|
||||||
|
Future<bool?> showAbortDialog() async {
|
||||||
Future<void> completeProfile() async =>
|
DialogResponse? response = await _dialogService.showDialog(
|
||||||
await runBusyFuture<Map<String, dynamic>>(_completeProfile());
|
cancelTitle: 'No',
|
||||||
|
buttonTitle: 'Yes',
|
||||||
Future<Map<String, dynamic>> _completeProfile() async {
|
barrierDismissible: true,
|
||||||
UserModel? user = await _authenticationService.getUser();
|
title: 'Abort Assessment',
|
||||||
Map<String, dynamic> response =
|
cancelTitleColor: kcDarkGrey,
|
||||||
await _apiService.updateProfile(data: _userData, user: user);
|
buttonTitleColor: kcPrimaryColor,
|
||||||
if (response['status'] == ResponseStatus.success) {
|
description: 'Are you sure to abort the assessment ?',
|
||||||
showSuccessToast(response['message']);
|
);
|
||||||
await replaceWithHome();
|
return response?.confirmed;
|
||||||
} else {
|
|
||||||
showErrorToast(response['message']);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation
|
Future<void> abort() async {
|
||||||
|
bool? response = await showAbortDialog();
|
||||||
|
if (response != null && response) {
|
||||||
|
next(page: 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Question navigation
|
||||||
void nextQuestion() {
|
void nextQuestion() {
|
||||||
_currentQuestion++;
|
_currentQuestion++;
|
||||||
Map<String, dynamic> response = evaluateAssessment();
|
Map<String, dynamic> response = evaluateAssessment();
|
||||||
|
|
||||||
|
if (_currentQuestion == _assessments.length) {
|
||||||
|
_proficiencyLevel = response['level'];
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
if (response['level'] == ProficiencyLevels.none) {
|
if (response['level'] == ProficiencyLevels.none) {
|
||||||
_pageController.jumpToPage(_currentQuestion);
|
_pageController.jumpToPage(_currentQuestion);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -203,6 +192,8 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +221,7 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop() {
|
void pop() {
|
||||||
if (_currentPage == 3 /*7*/) {
|
if (_currentPage == 0 || _currentPage == 3 /*7*/) {
|
||||||
_navigationService.back();
|
_navigationService.back();
|
||||||
} else if (_currentPage != 0 && _currentPage != 3) {
|
} else if (_currentPage != 0 && _currentPage != 3) {
|
||||||
_currentPage--;
|
_currentPage--;
|
||||||
|
|
@ -238,29 +229,45 @@ class AssessmentViewModel extends BaseViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> showAbortDialog() async {
|
// Navigation
|
||||||
DialogResponse? response = await _dialogService.showDialog(
|
|
||||||
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<void> abort() async {
|
|
||||||
bool? response = await showAbortDialog();
|
|
||||||
if (response != null && response) {
|
|
||||||
next(page: 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
||||||
|
|
||||||
|
Future<void> _completeProfile() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
Map<String, dynamic> response =
|
||||||
|
await _apiService.completeProfile(_userData);
|
||||||
|
if (response['status'] == ResponseStatus.success) {
|
||||||
|
showSuccessToast(response['message']);
|
||||||
|
clearUserData();
|
||||||
|
await replaceWithHome();
|
||||||
|
} else {
|
||||||
|
showErrorToast(response['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -63,7 +66,8 @@ class AssessmentFormScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
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}) =>
|
||||||
|
|
@ -106,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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -124,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]),
|
||||||
''),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -160,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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,18 +64,14 @@ class AssessmentIntroScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
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(
|
||||||
|
|
@ -95,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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,8 @@ 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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -64,19 +65,15 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
verticalSpaceSmall,
|
verticalSpaceSmall,
|
||||||
_buildPrimarySubTitle(),
|
_buildPrimarySubTitle(),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildIcon(),
|
_buildIconWrapper(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildSecondarySubTitle()
|
_buildSecondarySubTitle()
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildTitle(AssessmentViewModel viewModel) => Text(
|
Widget _buildTitle(AssessmentViewModel viewModel) => Text(
|
||||||
'You’re likely a ${viewModel.proficiencyLevel.name.toUpperCase()} speaker!',
|
'You’re 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() => const Text(
|
||||||
|
|
@ -85,12 +82,18 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: TextStyle(color: kcMediumGrey),
|
||||||
);
|
);
|
||||||
|
|
||||||
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(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
|
|
||||||
viewModel.addUserData(data);
|
viewModel.addUserData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
await viewModel.completeProfile();
|
await viewModel.completeProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,28 +86,17 @@ class StartLessonScreen extends ViewModelWidget<AssessmentViewModel> {
|
||||||
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', style: style25DG600, children: [
|
||||||
text: 'Welcome aboard',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ', ${viewModel.userData['first_name']}!',
|
text: ', ${viewModel.userData['first_name']}!',
|
||||||
style: const TextStyle(
|
style: style25DG600,
|
||||||
fontSize: 25,
|
|
||||||
color: kcPrimaryColor,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildSubTitle() => const Text(
|
Widget _buildSubTitle() => Text(
|
||||||
'You’re ready to explore your personalized lessons.',
|
'You’re ready to explore your personalized lessons.',
|
||||||
style: TextStyle(color: kcMediumGrey),
|
style: style14MG400,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
Widget _buildContinueButtonWrapper(AssessmentViewModel viewModel) => Padding(
|
||||||
|
|
|
||||||
|
|
@ -7,22 +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/common/ui_helpers.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
|
||||||
import 'package:yimaru_app/ui/views/login/login_view.dart';
|
|
||||||
import 'package:yimaru_app/ui/views/startup/startup_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;
|
||||||
|
|
@ -50,13 +58,6 @@ class HomeViewModel extends BaseViewModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveFullName(String name) async {
|
|
||||||
Map<String, dynamic> data = {
|
|
||||||
'firstName': name,
|
|
||||||
};
|
|
||||||
await _authenticationService.saveFullName(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> saveProfileStatus(bool value) async =>
|
Future<void> saveProfileStatus(bool value) async =>
|
||||||
await _authenticationService.saveProfileStatus(value);
|
await _authenticationService.saveProfileStatus(value);
|
||||||
|
|
||||||
|
|
@ -70,42 +71,48 @@ class HomeViewModel extends BaseViewModel {
|
||||||
await _navigationService.replaceWithOnboardingView();
|
await _navigationService.replaceWithOnboardingView();
|
||||||
|
|
||||||
// Remote api calls
|
// Remote api calls
|
||||||
Future<void> getProfileData() async =>
|
|
||||||
await runBusyFuture<Map<String, dynamic>>(_getProfileData());
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _getProfileData() async {
|
// Profile data
|
||||||
|
Future<void> getProfileData() async => await runBusyFuture(_getProfileData());
|
||||||
|
|
||||||
|
Future<void> _getProfileData() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
Map<String, dynamic> response = {};
|
Map<String, dynamic> response = {};
|
||||||
|
|
||||||
UserModel? user = await _authenticationService.getUser();
|
if (_user?.profileCompleted != null &&
|
||||||
|
(_user?.profileCompleted ?? false)) {
|
||||||
if (user?.profileCompleted != null) {
|
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
response = await _apiService.getProfileData(user?.userId);
|
response = await _apiService.getProfileData(_user?.userId);
|
||||||
|
|
||||||
if (response['status'] == ResponseStatus.success) {
|
if (response['status'] == ResponseStatus.success) {
|
||||||
Map<String, dynamic> data = {
|
UserModel user = response['data'] as UserModel;
|
||||||
'firstName': response['data']['first_name']
|
|
||||||
};
|
String image =
|
||||||
await _authenticationService.saveFullName(data);
|
await _imageDownloaderService.downloader(user.profilePicture);
|
||||||
|
|
||||||
|
await _authenticationService.saveUserData(image: image, data: user);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
// Profile status
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> getProfileStatus() async =>
|
Future<void> getProfileStatus() async =>
|
||||||
await runBusyFuture(_getProfileStatus());
|
await runBusyFuture(_getProfileStatus());
|
||||||
|
|
||||||
Future<void> _getProfileStatus() async {
|
Future<void> _getProfileStatus() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
Map<String, dynamic> response = {};
|
Map<String, dynamic> response = {};
|
||||||
|
|
||||||
UserModel? user = await _authenticationService.getUser();
|
if (_user?.profileCompleted == null) {
|
||||||
if (user?.profileCompleted == null) {
|
|
||||||
if (await _statusChecker.checkConnection()) {
|
if (await _statusChecker.checkConnection()) {
|
||||||
response = await _apiService.getProfileStatus(user);
|
response = await _apiService.getProfileStatus(_user);
|
||||||
} else {
|
} else {
|
||||||
await replaceWithFailure();
|
await replaceWithFailure();
|
||||||
}
|
}
|
||||||
|
} else if (!(_user?.profileCompleted ?? false)) {
|
||||||
|
response = {'data': false, 'status': ResponseStatus.success};
|
||||||
} else {
|
} else {
|
||||||
response = {'data': true, 'status': ResponseStatus.success};
|
response = {'data': true, 'status': ResponseStatus.success};
|
||||||
}
|
}
|
||||||
|
|
@ -117,4 +124,5 @@ class HomeViewModel extends BaseViewModel {
|
||||||
await saveProfileStatus(response['data']);
|
await saveProfileStatus(response['data']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class LearnView extends StackedView<LearnViewModel> {
|
||||||
|
|
||||||
Widget _buildAppBar(LearnViewModel viewModel) => LearnAppBar(
|
Widget _buildAppBar(LearnViewModel viewModel) => LearnAppBar(
|
||||||
name: viewModel.user?.firstName,
|
name: viewModel.user?.firstName,
|
||||||
profileImage: viewModel.user?.profileImage,
|
profileImage: viewModel.user?.profilePicture,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
|
Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) =>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,13 +8,22 @@ 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
|
// Navigation
|
||||||
|
|
@ -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.saveBasicUserData(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,65 @@ class LoginViewModel extends FormViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
Future<void> navigateToRegister() async =>
|
Future<void> navigateToRegister() async =>
|
||||||
await _navigationService.navigateToRegisterView();
|
await _navigationService.navigateToRegisterView();
|
||||||
|
|
||||||
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());
|
||||||
|
|
||||||
|
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> googleLogin() async => await runBusyFuture(_googleLogin());
|
||||||
|
|
||||||
|
Future<void> _googleLogin() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
GoogleSignInAccount? googleUser = await _googleAuthService.googleSignIn();
|
||||||
|
|
||||||
|
Map<String, dynamic> data = {
|
||||||
|
'id_token': googleUser?.authentication.idToken ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Map<String, dynamic> response = await _apiService.googleLogin(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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class LoginWithEmailScreen extends ViewModelWidget<LoginViewModel> {
|
||||||
};
|
};
|
||||||
viewModel.addUserData(data);
|
viewModel.addUserData(data);
|
||||||
|
|
||||||
await viewModel.login();
|
await viewModel.emailLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -164,13 +164,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,9 +186,21 @@ 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.googleLogin(),
|
||||||
|
);
|
||||||
|
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -79,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;
|
||||||
|
|
||||||
|
|
@ -261,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();
|
||||||
|
|
@ -362,7 +373,6 @@ class OnboardingViewModel extends FormViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
|
||||||
Future<void> navigateToLanguage() async =>
|
Future<void> navigateToLanguage() async =>
|
||||||
await _navigationService.navigateToLanguageView();
|
await _navigationService.navigateToLanguageView();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ class CountryRegionFormScreen extends ViewModelWidget<OnboardingViewModel> {
|
||||||
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 +113,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(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ 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/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_circular_progress_indicator.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';
|
||||||
|
|
@ -120,13 +119,12 @@ class ProfileView extends StackedView<ProfileViewModel> {
|
||||||
_buildProfileName(viewModel),
|
_buildProfileName(viewModel),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
Widget _buildProfileImage(
|
Widget _buildProfileImage(
|
||||||
{required BuildContext context,
|
{required BuildContext context,
|
||||||
required ProfileViewModel viewModel}) =>
|
required ProfileViewModel viewModel}) =>
|
||||||
ProfileImage(
|
ProfileImage(
|
||||||
profileImage: viewModel.user?.profileImage,
|
profileImage: viewModel.user?.profilePicture,
|
||||||
loading: viewModel.busy(StateObjects.profileImage) ? true:false,
|
loading: viewModel.busy(StateObjects.profileImage) ? true : false,
|
||||||
onTap: () async =>
|
onTap: () async =>
|
||||||
await _showImagePicker(context: context, viewModel: viewModel),
|
await _showImagePicker(context: context, viewModel: viewModel),
|
||||||
);
|
);
|
||||||
|
|
@ -196,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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,18 @@ import 'package:yimaru_app/ui/common/enmus.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../models/user_model.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 {
|
class ProfileViewModel extends ReactiveViewModel {
|
||||||
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
final _dialogService = locator<DialogService>();
|
||||||
|
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
final _imagePickerService = locator<ImagePickerService>();
|
final _imagePickerService = locator<ImagePickerService>();
|
||||||
|
|
@ -20,36 +29,62 @@ class ProfileViewModel extends ReactiveViewModel {
|
||||||
[_authenticationService];
|
[_authenticationService];
|
||||||
|
|
||||||
// Current user
|
// Current user
|
||||||
UserModel? get user => _authenticationService.user;
|
UserModel? get _user => _authenticationService.user;
|
||||||
|
|
||||||
|
UserModel? get user => _user;
|
||||||
|
|
||||||
// Image picker
|
// Image picker
|
||||||
Future<void> openCamera() async => runBusyFuture(_openCamera(),busyObject: StateObjects.profileImage);
|
Future<void> openCamera() async =>
|
||||||
|
runBusyFuture(_openCamera(), busyObject: StateObjects.profileImage);
|
||||||
|
|
||||||
Future<void> _openCamera()async{
|
Future<void> _openCamera() async {
|
||||||
String? image = await _imagePickerService.camera();
|
String? image = await _imagePickerService.camera();
|
||||||
|
pop();
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
|
await updateProfilePicture(image);
|
||||||
await _authenticationService.saveProfileImage(image);
|
await _authenticationService.saveProfileImage(image);
|
||||||
}
|
}
|
||||||
pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openGallery() async => runBusyFuture(_openGallery(),busyObject: StateObjects.profileImage);
|
Future<void> openGallery() async =>
|
||||||
|
runBusyFuture(_openGallery(), busyObject: StateObjects.profileImage);
|
||||||
|
|
||||||
Future<void> _openGallery() async {
|
Future<void> _openGallery() async {
|
||||||
String? image = await _imagePickerService.gallery();
|
String? image = await _imagePickerService.gallery();
|
||||||
|
pop();
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
|
await updateProfilePicture(image);
|
||||||
await _authenticationService.saveProfileImage(image);
|
await _authenticationService.saveProfileImage(image);
|
||||||
}
|
}
|
||||||
pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
Future<void> logOut() async {
|
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
|
// Navigation
|
||||||
void pop() => _navigationService.back();
|
void pop() => _navigationService.back();
|
||||||
|
|
||||||
|
|
@ -67,4 +102,19 @@ class ProfileViewModel extends ReactiveViewModel {
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
_buildProfileImageWrapper(viewModel),
|
_buildProfileImageWrapper(context: context, viewModel: viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
_buildNameFormSection(viewModel),
|
_buildNameFormSection(viewModel),
|
||||||
verticalSpaceMedium,
|
verticalSpaceMedium,
|
||||||
|
|
@ -120,12 +197,31 @@ class ProfileDetailView extends StackedView<ProfileDetailViewModel>
|
||||||
_buildLowerColumn(viewModel)
|
_buildLowerColumn(viewModel)
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _buildProfileImageWrapper(ProfileDetailViewModel viewModel) =>
|
Widget _buildProfileImageWrapper(
|
||||||
Align(alignment: Alignment.center, child: _buildProfileImage(viewModel));
|
{required BuildContext context,
|
||||||
|
required ProfileDetailViewModel viewModel}) =>
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: _buildProfileImage(context: context, viewModel: viewModel));
|
||||||
|
|
||||||
Widget _buildProfileImage(ProfileDetailViewModel viewModel) => ProfileImage(
|
Widget _buildProfileImage(
|
||||||
profileImage: viewModel.user?.profileImage,
|
{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,
|
||||||
children: _buildNameFormChildren(viewModel),
|
children: _buildNameFormChildren(viewModel),
|
||||||
|
|
@ -371,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,
|
||||||
|
|
@ -416,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,
|
||||||
|
|
@ -474,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) =>
|
||||||
|
|
@ -507,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) =>
|
||||||
|
|
@ -525,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(
|
||||||
|
|
@ -533,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),
|
||||||
|
|
@ -548,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(
|
||||||
|
|
@ -567,12 +689,16 @@ 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.isBusy ? const PageLoadingIndicator() : Container();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,32 @@ import 'package:stacked_services/stacked_services.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../models/user_model.dart';
|
import '../../../models/user_model.dart';
|
||||||
|
import '../../../services/api_service.dart';
|
||||||
import '../../../services/authentication_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>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
late final UserModel? _user = _authenticationService.user;
|
@override
|
||||||
|
List<ListenableServiceMixin> get listenableServices =>
|
||||||
|
[_authenticationService];
|
||||||
|
|
||||||
|
// Current user
|
||||||
|
UserModel? get _user => _authenticationService.user;
|
||||||
|
|
||||||
UserModel? get user => _user;
|
UserModel? get user => _user;
|
||||||
|
|
||||||
|
|
@ -44,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;
|
||||||
|
|
@ -81,15 +119,131 @@ 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());
|
||||||
|
|
||||||
|
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);
|
||||||
|
showSuccessToast(response['message']);
|
||||||
|
pop();
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ 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': 'You’ve mastered everyday English basics!',
|
'subTitle': 'You’ve mastered everyday English basics!',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -22,7 +22,7 @@ class ProgressViewModel extends BaseViewModel {
|
||||||
'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.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -30,7 +30,7 @@ class ProgressViewModel extends BaseViewModel {
|
||||||
'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': 'You’ve mastered everyday English basics!',
|
'subTitle': 'You’ve mastered everyday English basics!',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,15 @@ import 'package:yimaru_app/ui/views/home/home_view.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
import '../../../models/user_model.dart';
|
import '../../../models/user_model.dart';
|
||||||
|
import '../../../services/status_checker_service.dart';
|
||||||
|
|
||||||
class RegisterViewModel extends FormViewModel {
|
class RegisterViewModel extends FormViewModel {
|
||||||
final _apiService = locator<ApiService>();
|
final _apiService = locator<ApiService>();
|
||||||
|
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
|
@ -208,67 +213,7 @@ class RegisterViewModel extends FormViewModel {
|
||||||
_userData.clear();
|
_userData.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remote api calls
|
// In-app navigation
|
||||||
Future<void> register() async {
|
|
||||||
Map<String, dynamic> response = await runBusyFuture<Map<String, dynamic>>(
|
|
||||||
_apiService.register(_userData));
|
|
||||||
|
|
||||||
if (response['status'] == ResponseStatus.success) {
|
|
||||||
goTo(page: 3);
|
|
||||||
showSuccessToast(response['message']);
|
|
||||||
} else {
|
|
||||||
showErrorToast(response['message']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> verifyOtp() async {
|
|
||||||
Map<String, dynamic> response =
|
|
||||||
await runBusyFuture<Map<String, dynamic>>(_verifyOtp());
|
|
||||||
|
|
||||||
if (response['status'] == ResponseStatus.success) {
|
|
||||||
await replaceWithHome();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> _verifyOtp() async {
|
|
||||||
Map<String, dynamic> response = await _apiService.verifyOtp(_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
|
|
||||||
};
|
|
||||||
|
|
||||||
// {
|
|
||||||
// 'userId': 10,
|
|
||||||
// 'accessToken': 'accessToken',
|
|
||||||
// 'refreshToken': 'refreshToken'
|
|
||||||
// }
|
|
||||||
|
|
||||||
await _authenticationService.saveBasicUserData(data);
|
|
||||||
showSuccessToast(response['message']);
|
|
||||||
} else {
|
|
||||||
showErrorToast(response['message']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> resendOtp() async {
|
|
||||||
resetButton();
|
|
||||||
|
|
||||||
Map<String, dynamic> response = await runBusyFuture<Map<String, dynamic>>(
|
|
||||||
_apiService.resendOtp(_userData));
|
|
||||||
|
|
||||||
if (response['status'] == ResponseStatus.success) {
|
|
||||||
showSuccessToast(response['message']);
|
|
||||||
} else {
|
|
||||||
showErrorToast(response['message']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
void goTo({required int page, RegistrationType? type}) {
|
void goTo({required int page, RegistrationType? type}) {
|
||||||
_currentIndex = page;
|
_currentIndex = page;
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
|
|
@ -297,6 +242,7 @@ class RegisterViewModel extends FormViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
Future<void> navigateToTermsAndConditions() async =>
|
Future<void> navigateToTermsAndConditions() async =>
|
||||||
await _navigationService.navigateToTermsAndConditionsView();
|
await _navigationService.navigateToTermsAndConditionsView();
|
||||||
|
|
||||||
|
|
@ -308,4 +254,61 @@ class RegisterViewModel extends FormViewModel {
|
||||||
|
|
||||||
Future<void> replaceWithHome() async =>
|
Future<void> replaceWithHome() async =>
|
||||||
await _navigationService.clearStackAndShowView(const HomeView());
|
await _navigationService.clearStackAndShowView(const HomeView());
|
||||||
|
|
||||||
|
// Remote api calls
|
||||||
|
|
||||||
|
// Register
|
||||||
|
Future<void> register() async => await runBusyFuture(_register());
|
||||||
|
|
||||||
|
Future<void> _register() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
Map<String, dynamic> response = await _apiService.register(_userData);
|
||||||
|
|
||||||
|
if (response['status'] == ResponseStatus.success) {
|
||||||
|
goTo(page: 3);
|
||||||
|
showSuccessToast(response['message']);
|
||||||
|
} else {
|
||||||
|
showErrorToast(response['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> verifyOtp() async => await runBusyFuture(_verifyOtp());
|
||||||
|
|
||||||
|
Future<void> _verifyOtp() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
Map<String, dynamic> response = await _apiService.verifyOtp(_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);
|
||||||
|
await replaceWithHome();
|
||||||
|
showSuccessToast(response['message']);
|
||||||
|
} else {
|
||||||
|
showErrorToast(response['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resend otp
|
||||||
|
Future<void> resendOtp() async => await runBusyFuture(_resendOtp());
|
||||||
|
|
||||||
|
Future<void> _resendOtp() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
resetButton();
|
||||||
|
|
||||||
|
Map<String, dynamic> response = await _apiService.resendOtp(_userData);
|
||||||
|
|
||||||
|
if (response['status'] == ResponseStatus.success) {
|
||||||
|
showSuccessToast(response['message']);
|
||||||
|
} else {
|
||||||
|
showErrorToast(response['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,13 @@ class StartupViewModel extends BaseViewModel {
|
||||||
final firstTimeInstall = await _authenticationService.isFirstTimeInstall();
|
final firstTimeInstall = await _authenticationService.isFirstTimeInstall();
|
||||||
|
|
||||||
if (firstTimeInstall) {
|
if (firstTimeInstall) {
|
||||||
_navigationService.replaceWithWelcomeView();
|
await _navigationService.replaceWithWelcomeView();
|
||||||
} else {
|
} else {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
_navigationService.replaceWithHomeView();
|
await _authenticationService.getUser();
|
||||||
|
await _navigationService.replaceWithHomeView();
|
||||||
} else {
|
} else {
|
||||||
_navigationService.replaceWithLoginView();
|
await _navigationService.replaceWithLoginView();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,19 @@ import 'package:yimaru_app/app/app.router.dart';
|
||||||
import 'package:yimaru_app/services/authentication_service.dart';
|
import 'package:yimaru_app/services/authentication_service.dart';
|
||||||
|
|
||||||
import '../../../app/app.locator.dart';
|
import '../../../app/app.locator.dart';
|
||||||
|
import '../../../services/status_checker_service.dart';
|
||||||
|
|
||||||
class WelcomeViewModel extends BaseViewModel {
|
class WelcomeViewModel extends BaseViewModel {
|
||||||
final _navigationService = locator<NavigationService>();
|
final _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
|
final _statusChecker = locator<StatusCheckerService>();
|
||||||
|
|
||||||
final _authenticationService = locator<AuthenticationService>();
|
final _authenticationService = locator<AuthenticationService>();
|
||||||
|
|
||||||
int _currentPage = 0;
|
int _currentPage = 0;
|
||||||
|
|
||||||
int get currentPage => _currentPage;
|
int get currentPage => _currentPage;
|
||||||
|
|
||||||
Future<void> setFirstTimeInstall() async {
|
|
||||||
await runBusyFuture(_setFirstTimeInstall());
|
|
||||||
}
|
|
||||||
|
|
||||||
// First time install
|
|
||||||
Future<void> _setFirstTimeInstall() async {
|
|
||||||
await _authenticationService.setFirstTimeInstall(false);
|
|
||||||
await navigateToLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
Future<void> navigateToLogin() async =>
|
Future<void> navigateToLogin() async =>
|
||||||
await _navigationService.navigateToLoginView();
|
await _navigationService.navigateToLoginView();
|
||||||
|
|
@ -32,4 +25,18 @@ class WelcomeViewModel extends BaseViewModel {
|
||||||
_currentPage++;
|
_currentPage++;
|
||||||
rebuildUi();
|
rebuildUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remote api call
|
||||||
|
|
||||||
|
// First time install
|
||||||
|
Future<void> setFirstTimeInstall() async {
|
||||||
|
await runBusyFuture(_setFirstTimeInstall());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setFirstTimeInstall() async {
|
||||||
|
if (await _statusChecker.checkConnection()) {
|
||||||
|
await _authenticationService.setFirstTimeInstall(false);
|
||||||
|
await navigateToLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import '../common/app_colors.dart';
|
import '../common/app_colors.dart';
|
||||||
import '../common/ui_helpers.dart';
|
import '../common/ui_helpers.dart';
|
||||||
import 'package:omni_datetime_picker/omni_datetime_picker.dart';
|
import 'package:omni_datetime_picker/omni_datetime_picker.dart';
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class CustomDropdownPicker extends StatelessWidget {
|
||||||
Widget build(BuildContext context) => _buildDropDownSearchWrapper();
|
Widget build(BuildContext context) => _buildDropDownSearchWrapper();
|
||||||
|
|
||||||
Widget _buildDropDownSearchWrapper() => Theme(
|
Widget _buildDropDownSearchWrapper() => Theme(
|
||||||
data: ThemeData().copyWith(cardColor: kcBackgroundColor),
|
data: ThemeData().copyWith(cardColor: const Color(0xfff5e9f4)),
|
||||||
child: _buildDropDownSearch(),
|
child: _buildDropDownSearch(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -41,7 +41,6 @@ class CustomDropdownPicker extends StatelessWidget {
|
||||||
showSearchBox: true,
|
showSearchBox: true,
|
||||||
showSelectedItems: true,
|
showSelectedItems: true,
|
||||||
searchFieldProps: _searchFieldProps(),
|
searchFieldProps: _searchFieldProps(),
|
||||||
menuProps: const MenuProps(color: kcBackgroundColor),
|
|
||||||
itemBuilder: (context, value, isSelected, isPicked) =>
|
itemBuilder: (context, value, isSelected, isPicked) =>
|
||||||
_buildPopupProsBuilderWrapper(value),
|
_buildPopupProsBuilderWrapper(value),
|
||||||
);
|
);
|
||||||
|
|
@ -49,10 +48,7 @@ class CustomDropdownPicker extends StatelessWidget {
|
||||||
TextFieldProps _searchFieldProps() => TextFieldProps(
|
TextFieldProps _searchFieldProps() => TextFieldProps(
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
style: const TextStyle(
|
style: style14DG400,
|
||||||
fontSize: 14,
|
|
||||||
color: kcLightGrey,
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
cursorColor: kcLightGrey,
|
cursorColor: kcLightGrey,
|
||||||
decoration: _popUpDecoration(),
|
decoration: _popUpDecoration(),
|
||||||
|
|
@ -60,18 +56,15 @@ class CustomDropdownPicker extends StatelessWidget {
|
||||||
|
|
||||||
InputDecoration _popUpDecoration() => InputDecoration(
|
InputDecoration _popUpDecoration() => InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
|
hintStyle: style14DG400,
|
||||||
|
fillColor: kcTransparent,
|
||||||
errorBorder: searchBorder,
|
errorBorder: searchBorder,
|
||||||
focusedBorder: searchBorder,
|
focusedBorder: searchBorder,
|
||||||
enabledBorder: searchBorder,
|
enabledBorder: searchBorder,
|
||||||
disabledBorder: searchBorder,
|
disabledBorder: searchBorder,
|
||||||
focusedErrorBorder: searchBorder,
|
focusedErrorBorder: searchBorder,
|
||||||
hintStyle: const TextStyle(
|
contentPadding: const EdgeInsets.only(top: 12),
|
||||||
fontSize: 14,
|
prefixIcon: icon != null ? _buildPrefixIcon() : null,
|
||||||
color: kcLightGrey,
|
|
||||||
),
|
|
||||||
fillColor: kcPrimaryColor.withOpacity(0.1),
|
|
||||||
prefix: icon != null ? _buildPrefixIcon() : null,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 0),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPopupProsBuilderWrapper(String value) => Padding(
|
Widget _buildPopupProsBuilderWrapper(String value) => Padding(
|
||||||
|
|
@ -98,30 +91,20 @@ class CustomDropdownPicker extends StatelessWidget {
|
||||||
focusedBorder: border,
|
focusedBorder: border,
|
||||||
enabledBorder: border,
|
enabledBorder: border,
|
||||||
disabledBorder: border,
|
disabledBorder: border,
|
||||||
focusedErrorBorder: border,
|
hintStyle: style14LG400,
|
||||||
hintStyle: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: kcLightGrey,
|
|
||||||
),
|
|
||||||
fillColor: kcPrimaryColor.withOpacity(0.1),
|
fillColor: kcPrimaryColor.withOpacity(0.1),
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildPrefixIcon() => Container(
|
Widget _buildPrefixIcon() => Padding(
|
||||||
width: 30,
|
padding: const EdgeInsets.only(right: 10, left: 5),
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(border: rightBorder),
|
|
||||||
margin: const EdgeInsets.only(right: 25, bottom: 7.5, left: 5),
|
|
||||||
child: icon,
|
child: icon,
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildDropdownBuilder(String? value) => Text(
|
Widget _buildDropdownBuilder(String? value) => Text(
|
||||||
value ?? hint,
|
value ?? hint,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: const TextStyle(
|
style: style14DG400,
|
||||||
fontSize: 14,
|
|
||||||
color: kcDarkGrey,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
import 'package:yimaru_app/ui/common/ui_helpers.dart';
|
||||||
|
|
||||||
|
|
@ -40,7 +39,8 @@ class LearnAppBar extends StatelessWidget {
|
||||||
Widget _buildProfileImage() => CircleAvatar(
|
Widget _buildProfileImage() => CircleAvatar(
|
||||||
radius: 25,
|
radius: 25,
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
backgroundImage: profileImage != null
|
backgroundImage:
|
||||||
|
profileImage != null || (profileImage?.contains('.') ?? false)
|
||||||
? FileImage(
|
? FileImage(
|
||||||
File(profileImage!),
|
File(profileImage!),
|
||||||
)
|
)
|
||||||
|
|
@ -49,7 +49,9 @@ class LearnAppBar extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget? _buildImageBuilder() =>
|
Widget? _buildImageBuilder() =>
|
||||||
profileImage == null ? _buildPersonIcon() : null;
|
profileImage == null || !(profileImage?.contains('.') ?? false)
|
||||||
|
? _buildPersonIcon()
|
||||||
|
: null;
|
||||||
|
|
||||||
Widget _buildPersonIcon() => const Icon(
|
Widget _buildPersonIcon() => const Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yimaru_app/ui/widgets/progress_status.dart';
|
|
||||||
|
|
||||||
import '../common/app_colors.dart';
|
import '../common/app_colors.dart';
|
||||||
import '../common/enmus.dart';
|
import '../common/enmus.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yimaru_app/ui/common/app_colors.dart';
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
|
|
||||||
|
|
@ -38,7 +37,7 @@ class ProfileImage extends StatelessWidget {
|
||||||
backgroundColor: kcPrimaryColor,
|
backgroundColor: kcPrimaryColor,
|
||||||
backgroundImage: loading
|
backgroundImage: loading
|
||||||
? null
|
? null
|
||||||
: profileImage != null
|
: profileImage != null || (profileImage?.contains('.') ?? false)
|
||||||
? FileImage(
|
? FileImage(
|
||||||
File(profileImage!),
|
File(profileImage!),
|
||||||
)
|
)
|
||||||
|
|
@ -48,7 +47,7 @@ class ProfileImage extends StatelessWidget {
|
||||||
|
|
||||||
Widget? _buildImageBuilder() => loading
|
Widget? _buildImageBuilder() => loading
|
||||||
? null
|
? null
|
||||||
: profileImage == null
|
: profileImage == null || !(profileImage?.contains('.') ?? false)
|
||||||
? _buildPersonIcon()
|
? _buildPersonIcon()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
|
||||||
19
lib/ui/widgets/refresh_button.dart
Normal file
19
lib/ui/widgets/refresh_button.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yimaru_app/ui/common/app_colors.dart';
|
||||||
|
|
||||||
|
class RefreshButton extends StatelessWidget {
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
|
const RefreshButton({super.key, this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Center(child: _buildButton());
|
||||||
|
|
||||||
|
Widget _buildButton() => IconButton(onPressed: onTap, icon: _buildIcon());
|
||||||
|
|
||||||
|
Widget _buildIcon() => const Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
size: 100,
|
||||||
|
color: kcPrimaryColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,17 @@ import Foundation
|
||||||
import battery_plus
|
import battery_plus
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
|
import firebase_core
|
||||||
import flutter_secure_storage_darwin
|
import flutter_secure_storage_darwin
|
||||||
|
import google_sign_in_ios
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
|
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||||
|
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
84
pubspec.lock
84
pubspec.lock
|
|
@ -153,6 +153,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
change_app_package_name:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: change_app_package_name
|
||||||
|
sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -361,6 +369,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.3+5"
|
version: "0.9.3+5"
|
||||||
|
firebase_core:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_core
|
||||||
|
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.4.0"
|
||||||
|
firebase_core_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_core_platform_interface
|
||||||
|
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
|
firebase_core_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_core_web
|
||||||
|
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -536,6 +568,54 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.0"
|
version: "0.15.0"
|
||||||
|
google_identity_services_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_identity_services_web
|
||||||
|
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3+1"
|
||||||
|
google_sign_in:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: google_sign_in
|
||||||
|
sha256: "521031b65853b4409b8213c0387d57edaad7e2a949ce6dea0d8b2afc9cb29763"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.0"
|
||||||
|
google_sign_in_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_android
|
||||||
|
sha256: "5ec98ab35387c68c0050495bb211bd88375873723a80fae7c2e9266ea0bdd8bb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.7"
|
||||||
|
google_sign_in_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_ios
|
||||||
|
sha256: "234fc2830b55d1bbeb7e05662967691f5994143ff43dc70d3f139d1bbb3b8fb2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.2.5"
|
||||||
|
google_sign_in_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_platform_interface
|
||||||
|
sha256: "7f59208c42b415a3cca203571128d6f84f885fead2d5b53eb65a9e27f2965bb5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
|
google_sign_in_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: google_sign_in_web
|
||||||
|
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -873,7 +953,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
|
@ -889,7 +969,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
intl: any
|
intl: any
|
||||||
dio: ^5.9.0
|
dio: ^5.9.0
|
||||||
|
path: ^1.9.1
|
||||||
pinput: ^6.0.1
|
pinput: ^6.0.1
|
||||||
stacked: ^3.4.0
|
stacked: ^3.4.0
|
||||||
iconsax: ^0.0.8
|
iconsax: ^0.0.8
|
||||||
|
|
@ -21,7 +22,10 @@ dependencies:
|
||||||
storage_info: ^1.0.0
|
storage_info: ^1.0.0
|
||||||
flutter_html: ^3.0.0
|
flutter_html: ^3.0.0
|
||||||
email_validator: any
|
email_validator: any
|
||||||
|
firebase_core: ^4.4.0
|
||||||
in_app_update: ^4.2.5
|
in_app_update: ^4.2.5
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
google_sign_in: ^7.2.0
|
||||||
toastification: ^3.0.3
|
toastification: ^3.0.3
|
||||||
dropdown_search: ^6.0.2
|
dropdown_search: ^6.0.2
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
|
|
@ -30,9 +34,9 @@ dependencies:
|
||||||
json_serializable: ^6.8.0
|
json_serializable: ^6.8.0
|
||||||
permission_handler: ^12.0.1
|
permission_handler: ^12.0.1
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
|
change_app_package_name: ^1.5.0
|
||||||
flutter_secure_storage: ^10.0.0
|
flutter_secure_storage: ^10.0.0
|
||||||
flutter_timer_countdown: ^1.0.7
|
flutter_timer_countdown: ^1.0.7
|
||||||
|
|
||||||
internet_connection_checker_plus: ^2.9.1+2
|
internet_connection_checker_plus: ^2.9.1+2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import 'package:yimaru_app/services/dio_service.dart';
|
||||||
import 'package:yimaru_app/services/status_checker_service.dart';
|
import 'package:yimaru_app/services/status_checker_service.dart';
|
||||||
import 'package:yimaru_app/services/permission_handler_service.dart';
|
import 'package:yimaru_app/services/permission_handler_service.dart';
|
||||||
import 'package:yimaru_app/services/image_picker_service.dart';
|
import 'package:yimaru_app/services/image_picker_service.dart';
|
||||||
|
import 'package:yimaru_app/services/google_auth_service.dart';
|
||||||
|
import 'package:yimaru_app/services/image_downloader_service.dart';
|
||||||
// @stacked-import
|
// @stacked-import
|
||||||
|
|
||||||
import 'test_helpers.mocks.dart';
|
import 'test_helpers.mocks.dart';
|
||||||
|
|
@ -27,6 +29,9 @@ import 'test_helpers.mocks.dart';
|
||||||
MockSpec<PermissionHandlerService>(
|
MockSpec<PermissionHandlerService>(
|
||||||
onMissingStub: OnMissingStub.returnDefault),
|
onMissingStub: OnMissingStub.returnDefault),
|
||||||
MockSpec<ImagePickerService>(onMissingStub: OnMissingStub.returnDefault),
|
MockSpec<ImagePickerService>(onMissingStub: OnMissingStub.returnDefault),
|
||||||
|
MockSpec<GoogleAuthService>(onMissingStub: OnMissingStub.returnDefault),
|
||||||
|
MockSpec<ImageDownloaderService>(
|
||||||
|
onMissingStub: OnMissingStub.returnDefault),
|
||||||
// @stacked-mock-spec
|
// @stacked-mock-spec
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
@ -41,6 +46,8 @@ void registerServices() {
|
||||||
getAndRegisterStatusCheckerService();
|
getAndRegisterStatusCheckerService();
|
||||||
getAndRegisterPermissionHandlerService();
|
getAndRegisterPermissionHandlerService();
|
||||||
getAndRegisterImagePickerService();
|
getAndRegisterImagePickerService();
|
||||||
|
getAndRegisterGoogleAuthService();
|
||||||
|
getAndRegisterImageDownloaderService();
|
||||||
// @stacked-mock-register
|
// @stacked-mock-register
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +153,20 @@ MockImagePickerService getAndRegisterImagePickerService() {
|
||||||
locator.registerSingleton<ImagePickerService>(service);
|
locator.registerSingleton<ImagePickerService>(service);
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MockGoogleAuthService getAndRegisterGoogleAuthService() {
|
||||||
|
_removeRegistrationIfExists<GoogleAuthService>();
|
||||||
|
final service = MockGoogleAuthService();
|
||||||
|
locator.registerSingleton<GoogleAuthService>(service);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
MockImageDownloaderService getAndRegisterImageDownloaderService() {
|
||||||
|
_removeRegistrationIfExists<ImageDownloaderService>();
|
||||||
|
final service = MockImageDownloaderService();
|
||||||
|
locator.registerSingleton<ImageDownloaderService>(service);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
// @stacked-mock-create
|
// @stacked-mock-create
|
||||||
|
|
||||||
void _removeRegistrationIfExists<T extends Object>() {
|
void _removeRegistrationIfExists<T extends Object>() {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
11
test/services/google_auth_service_test.dart
Normal file
11
test/services/google_auth_service_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
|
||||||
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GoogleAuthServiceTest -', () {
|
||||||
|
setUp(() => registerServices());
|
||||||
|
tearDown(() => locator.reset());
|
||||||
|
});
|
||||||
|
}
|
||||||
11
test/services/image_downloader_service_test.dart
Normal file
11
test/services/image_downloader_service_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:yimaru_app/app/app.locator.dart';
|
||||||
|
|
||||||
|
import '../helpers/test_helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ImageDownloaderServiceTest -', () {
|
||||||
|
setUp(() => registerServices());
|
||||||
|
tearDown(() => locator.reset());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <battery_plus/battery_plus_windows_plugin.h>
|
#include <battery_plus/battery_plus_windows_plugin.h>
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
|
|
||||||
|
|
@ -19,6 +20,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
battery_plus
|
battery_plus
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
firebase_core
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user