Compare commits

...

60 Commits

Author SHA1 Message Date
misha
54c27c28cd feat(test): updated tests to increase tests coverage 2026-05-06 14:12:34 +03:00
misha
fe243965e3 feat(test): 70% tests coverage 2026-05-06 05:27:25 +03:00
misha
56313a4fcc Merge branch 'issue-974' of ssh://10.55.5.123:222/misha/eatvio_app into issue-974 2026-05-05 10:52:00 +03:00
misha
cb36ef8e07 Merge remote-tracking branch 'upstream/main' into issue-974 2026-05-05 10:51:20 +03:00
misha
b2072ab626 feat(video_calls): fixed falling test 2026-05-05 10:40:11 +03:00
5b72684c95 merge upstream 2026-05-05 05:17:22 +00:00
misha
91ad51d340 feat(video_calls): first impl for main video calls screen 2026-05-05 08:09:24 +03:00
5d86b7c71b Merge pull request 'version changed' (#971) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#971
2026-04-17 14:17:46 +00:00
misha
50aefde842 version changed 2026-04-17 17:17:04 +03:00
5fbd578463 Merge pull request 'ci/cd: fastlane version' (#970) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#970
2026-04-17 11:13:08 +00:00
misha
426bd49aa1 ci/cd: fastlane version 2026-04-17 14:12:10 +03:00
c6fc20d217 Merge pull request 'version changed' (#968) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#968
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2026-04-17 08:30:02 +00:00
misha
17e18dc68b version changed 2026-04-17 11:08:18 +03:00
d483295aab Merge pull request 'feat(pushNotifications): fixed push notifications navigation' (#948) from misha/eatvio_app:push-notifications-navigation into main
Reviewed-on: eatvio/eatvio_app#948
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2026-04-16 09:38:30 +00:00
misha
c8284338aa feat(image_compress_utility): adde test for corupted images 2026-04-15 16:27:27 +03:00
misha
03ec91a595 feat(image_compress_utility): improved compressing logic 2026-04-15 15:50:09 +03:00
misha
e814683984 version changed 2026-04-14 09:44:12 +03:00
misha
684415ef5b feat(setting): added profile data apdating for sttings screen, added validation for general info form 2026-04-14 09:28:02 +03:00
misha
631ceb2b7a version changed 2026-04-08 12:47:07 +03:00
misha
71a405c63f feat(notifications): changed navigation flow from Cancelled diet and started diet notifications 2026-04-08 12:43:15 +03:00
misha
4ca90eb2df feat(push_navigation): fixed navigation to chat screen 2026-04-08 12:14:43 +03:00
misha
f17f1ecb57 version changed 2026-04-07 10:32:10 +03:00
misha
a71bcc752d feat(fixes): fixed analyzer issue 2026-04-07 10:06:04 +03:00
misha
aac085def4 feat(app): refactoring of sentry using, feat(become_consultant): fixed possible infinite loading, feat(push_notifications): fixed push navigation from paid plan push 2026-04-07 10:05:11 +03:00
misha
b6b13b1db5 version update 2026-04-01 13:21:19 +03:00
misha
6629286bf1 feat(CI/CD): changed gitea user token name 2026-04-01 10:59:54 +03:00
misha
966bc23f0b feat(CI/CD): removed proxy server 2026-04-01 10:49:50 +03:00
misha
a13b623ca2 feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage 2026-04-01 10:37:59 +03:00
misha
ee2dd75a94 feat(ci/cd): added test coverage verification 2026-03-31 10:20:43 +03:00
misha
c450a41715 feat(ci/cd): added token for repo 2026-03-30 10:33:46 +03:00
misha
55b6b75134 feat(ci/cd): env repo changed 2026-03-30 10:16:13 +03:00
misha
335e36dd18 feat(pushNotifications): fixed push notifications navigation 2026-03-26 18:06:20 +03:00
7106bde4ae Merge pull request 'fix 939, 927' (#940) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#940
Reviewed-by: misha <misha@noreply.localhost>
2026-03-26 08:58:17 +00:00
mso7nueh
4f241f7db6 feat: try push handling 2026-03-18 15:39:13 +03:00
mso7nueh
3d7755a9a2 feat: Introduce AppRouter for centralized application routing and deep link handling, and create push_navigation_handler for push notification navigation. 2026-03-17 16:33:54 +03:00
mso7nueh
decc8c63e6 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:32:31 +03:00
mso7nueh
ad5b48ef1b Fix merge conflicts with upstream main by retaining list refresh logic 2026-03-17 12:32:02 +03:00
mso7nueh
e8dc0138e2 Merge pull request 'feat(recipes, diets)' (#938) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#938
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-17 09:19:25 +00:00
mso7nueh
96896671b6 merge upstream 2026-03-17 09:18:48 +00:00
mso7nueh
5e2f0430d9 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:18:25 +03:00
mso7nueh
2da3b10e63 feat: Implement 'My Recipes' feature, add diary moderation request modal, and introduce diary repository. 2026-03-17 12:18:17 +03:00
misha
5cb121d733 fat(recipes, diets): added list refreshing 2026-03-16 10:32:12 +03:00
mso7nueh
109013c25c Merge pull request 'feat(diary)' (#931) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#931
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-12 13:49:32 +00:00
misha
f5d14d213f Merge remote-tracking branch 'upstream/main' 2026-03-12 16:04:58 +03:00
misha
26ae28a705 feat(tests): fixed test errors 2026-03-12 14:41:16 +03:00
mso7nueh
7672477c03 Merge pull request 'Обновить pubspec.yaml' (#932) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#932
2026-03-11 12:15:49 +00:00
mso7nueh
07277d6e48 Обновить pubspec.yaml 2026-03-11 12:15:33 +00:00
misha
9d1bedff26 feat(diary): fixed consultant portion comments and day finishing errors handling 2026-03-11 11:55:10 +03:00
misha
e39638be7f feat(notifications): removed unknown notification from layoute 2026-03-05 13:44:33 +03:00
442fc0ec14 Merge pull request 'feat(consultant_registration)' (#924) from misha/eatvio_app:issue-914 into main
Reviewed-on: eatvio/eatvio_app#924
2026-03-04 07:56:21 +00:00
a8f8cc8eb0 Merge pull request 'feat(notifications)' (#921) from misha/eatvio_app:issue-911 into main
Reviewed-on: eatvio/eatvio_app#921
2026-03-04 07:55:58 +00:00
misha
3892bf00aa fix: analizer 2026-03-04 09:25:11 +03:00
misha
89268e1132 Merge remote-tracking branch 'upstream/main' into issue-911 2026-03-04 09:08:17 +03:00
misha
08eeea6ba4 feat(notifications): added push notifications navigation handling and fixed ws notification parsing 2026-03-04 09:05:00 +03:00
misha
58d99424a0 Merge remote-tracking branch 'upstream/main' into issue-911 2026-02-27 13:15:09 +03:00
misha
c6cdb5bf01 Merge branch 'main' of ssh://10.55.5.123:222/misha/eatvio_app into issue-911 2026-02-27 13:14:47 +03:00
misha
8b1d08b2aa fix: ci/cd 2026-02-25 13:00:38 +03:00
misha
6f666a8859 feat(tests): fixed notifications tests 2026-02-18 20:19:23 +03:00
misha
c540cbe6df feat(notifications): added navigation from different types of notifications 2026-02-18 19:45:32 +03:00
mso7nueh
bb97c395dd feat: Implement comprehensive navigation logic for various notification types within the NotificationWidget. 2026-02-12 15:55:11 +03:00
259 changed files with 10284 additions and 3718 deletions

View File

@@ -8,6 +8,9 @@ jobs:
build: build:
runs-on: [macos-arm64] runs-on: [macos-arm64]
steps: steps:
- name: Fastlane version
run: fastlane --version
- name: Checkout - name: Checkout
uses: https://github.com/actions/checkout@v2 uses: https://github.com/actions/checkout@v2

View File

@@ -1,37 +1,39 @@
name: Eatvio App Android Test name: Eatvio App Android Test
on: [ pull_request ] on: [pull_request]
env: env:
flutter_version: "3.41.2" flutter_version: '3.41.2'
java_version: "21.x" java_version: '21.x'
flutter_channel: "stable" flutter_channel: 'stable'
my_local_cache_dir: "/opt/hostedtoolcache/" my_local_cache_dir: '/opt/hostedtoolcache/'
ANDROID_SDK_ROOT: "/opt/hostedtoolcache/Android/Sdk" ANDROID_SDK_ROOT: '/opt/hostedtoolcache/Android/Sdk'
jobs: jobs:
build: build:
runs-on: [ ubuntu-latest ] runs-on: [ubuntu-latest]
steps: steps:
- uses: https://github.com/actions/checkout@v3 - uses: https://github.com/actions/checkout@v3
- name: Set proxy server # Пока что не нужно проксирование
env: # - name: Set proxy server
PROXY_USER: ${{ secrets.PROXY_USER }} # env:
PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }} # PROXY_USER: ${{ secrets.PROXY_USER }}
run: | # PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
echo "Setting up proxy..." # run: |
cat <<EOF >/etc/apt/apt.conf.d/proxy.conf # echo "Setting up proxy..."
Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/"; # cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
EOF # Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
# EOF
- name: Checkout env repository - name: Checkout env repository
uses: https://github.com/actions/checkout@v3 uses: https://github.com/actions/checkout@v3
with: with:
repository: mso7nueh/eatvio_app_env repository: eatvio/eatvio_app_env
path: env_repo path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }} # Здесь может быть токен любого юзера с доступом в gitea
token: ${{ vars.READ_USER_GITEA_TOKEN }}
ref: main ref: main
- name: Copy .env file - name: Copy .env file
@@ -66,7 +68,7 @@ jobs:
- name: Cache Android SDK - name: Cache Android SDK
id: cache-sdk id: cache-sdk
uses: "https://github.com/actions/cache@v4" uses: 'https://github.com/actions/cache@v4'
with: with:
path: ${{ env.my_local_cache_dir }} path: ${{ env.my_local_cache_dir }}
key: ${{ runner.os }}-cache-sdk key: ${{ runner.os }}-cache-sdk
@@ -74,14 +76,14 @@ jobs:
- name: Set up JDK - name: Set up JDK
uses: https://github.com/actions/setup-java@v3 uses: https://github.com/actions/setup-java@v3
with: with:
distribution: "zulu" distribution: 'zulu'
java-version: ${{ env.java_version }} java-version: ${{ env.java_version }}
cache: gradle cache: gradle
- name: Install utils for Android SDK - name: Install utils for Android SDK
run: | run: |
apt-get update apt-get update
apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
- name: Install Android SDK - name: Install Android SDK
if: steps.cache-sdk.outputs.cache-hit != 'true' if: steps.cache-sdk.outputs.cache-hit != 'true'
@@ -100,15 +102,14 @@ jobs:
sdkmanager "platform-tools" "platforms;android-35" "platforms;android-34" "platforms;android-33" "platforms;android-31" sdkmanager "platform-tools" "platforms;android-35" "platforms;android-34" "platforms;android-33" "platforms;android-31"
sdkmanager "build-tools;34.0.0" sdkmanager "build-tools;34.0.0"
sdkmanager "ndk;25.1.8937393" sdkmanager "ndk;25.1.8937393"
sdkmanager --install "cmake;3.22.1" sdkmanager --install "cmake;3.22.1"
- name: Set up Android environment variables - name: Set up Android environment variables
id: "android-env-setup" id: 'android-env-setup'
run: | run: |
echo "ANDROID_SDK_ROOT=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV echo "ANDROID_SDK_ROOT=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
- name: Set up Flutter - name: Set up Flutter
run: | run: |
@@ -147,7 +148,7 @@ jobs:
if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi
git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter" git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter"
flutter config --android-sdk="$ANDROID_HOME" flutter config --android-sdk="$ANDROID_HOME"
flutter doctor -v flutter doctor -v
- name: Get dependencies - name: Get dependencies
run: | run: |
@@ -212,12 +213,11 @@ jobs:
percentage = (covered / total) * 100 percentage = (covered / total) * 100
printf "✅ Coverage: %.2f%%\n", percentage printf "✅ Coverage: %.2f%%\n", percentage
print "" print ""
if (percentage < 60) { if (percentage < 70) {
printf "⚠️ Warning: Coverage is below 60%%\n" printf "❌ FAIL: Coverage is below 70%%\n"
} else if (percentage < 80) { exit 1
printf "💛 Good: Coverage is above 60%%\n"
} else { } else {
printf "💚 Excellent: Coverage is above 80%%\n" printf "💚 Excellent: Coverage is above 70%%\n"
} }
} }
} }
@@ -236,7 +236,7 @@ jobs:
APK_NAME="eatvio_app_${TIMESTAMP}.apk" APK_NAME="eatvio_app_${TIMESTAMP}.apk"
mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME
echo "APK build finished $APK_NAME" echo "APK build finished $APK_NAME"
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
- name: Upload APK artifact - name: Upload APK artifact
uses: https://github.com/actions/upload-artifact@v3 uses: https://github.com/actions/upload-artifact@v3

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ env/.env
android/key.properties android/key.properties
app/upload-keystore.jks app/upload-keystore.jks
coverage/ coverage/
backend/
.fvm/ .fvm/
.fvmrc .fvmrc
/eatvio /eatvio

View File

@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
@@ -55,32 +57,19 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<!-- Main domains -->
<data android:host="eatvio.ru" /> <data android:host="eatvio.ru" />
<data android:host="www.eatvio.ru" /> <data android:host="www.eatvio.ru" />
<data android:host="eatvio.com" />
<data android:host="www.eatvio.com" />
<!-- Development/Subdomains -->
<data android:host="*.eatvio.ru" />
<data android:host="*.eatvio.com" />
<data android:pathPattern="/ru/recipe/.*" /> <!-- Simplify patterns to allow navigation within the app for all matching hosts -->
<data android:pathPattern="/en/recipe/.*" /> <data android:pathPattern="/.*" />
<data android:pathPattern="/ru/blog_post/.*" />
<data android:pathPattern="/en/blog_post/.*" />
<data android:pathPattern="/ru/diet/.*" />
<data android:pathPattern="/en/diet/.*" />
<data android:pathPattern="/ru/plan/.*" />
<data android:pathPattern="/en/plan/.*" />
<data android:pathPattern="/ru/chat/.*" />
<data android:pathPattern="/en/chat/.*" />
<data android:pathPattern="/ru/chat" />
<data android:pathPattern="/en/chat" />
<data android:pathPattern="/ru/profile/.*" />
<data android:pathPattern="/en/profile/.*" />
<data android:pathPattern="/ru/diary/.*" />
<data android:pathPattern="/en/diary/.*" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.

View File

@@ -1,6 +1,10 @@
package com.eatvioapp.app package com.eatvioapp.app
import android.app.PendingIntent
import android.content.Intent
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage import com.huawei.hms.push.RemoteMessage
@@ -13,6 +17,45 @@ class HmsPushService : HmsMessageService() {
override fun onMessageReceived(message: RemoteMessage?) { override fun onMessageReceived(message: RemoteMessage?) {
super.onMessageReceived(message) super.onMessageReceived(message)
Log.i("HMS", "onMessageReceived: ${message?.data}") Log.i("HMS", "onMessageReceived: ${message?.data}")
if (message == null) return
// Берём action_url из data-поля, если оно есть.
val actionUrl = message.dataOfMap["action_url"]
// Строим системное уведомление, по тапу по которому откроется MainActivity с action_url.
showNotification(message, actionUrl)
}
private fun showNotification(message: RemoteMessage, actionUrl: String?) {
val context = applicationContext
// Интент для запуска Flutter-активити.
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
if (!actionUrl.isNullOrBlank()) {
putExtra("action_url", actionUrl)
}
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val channelId = "hms_push_channel"
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.mipmap.ic_launcher) // используем иконку приложения
.setContentTitle(message.notification?.title ?: context.getString(R.string.app_name))
.setContentText(message.notification?.body ?: "")
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
NotificationManagerCompat.from(context).notify(message.messageId.hashCode(), notification)
} }
} }

View File

@@ -1,8 +1,8 @@
package com.eatvioapp.app package com.eatvioapp.app
import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@@ -14,12 +14,13 @@ import kotlinx.coroutines.launch
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private val channelName = "com.eatvioapp/hms_push" private val channelName = "com.eatvioapp/hms_push"
private var methodChannel: MethodChannel? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName) methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
.setMethodCallHandler { call, result -> methodChannel?.setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"getHuaweiToken" -> { "getHuaweiToken" -> {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@@ -71,5 +72,33 @@ class MainActivity: FlutterActivity() {
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
// Обрабатываем возможный action_url, с которым была открыта Activity (например, из HMS уведомления).
handleIntentForActionUrl(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// Обработка нового интента, когда Activity уже запущена (тап по уведомлению).
handleIntentForActionUrl(intent)
}
private fun handleIntentForActionUrl(intent: Intent?) {
if (intent == null) return
// Вариант 1: action_url лежит в extras.
val fromExtras = intent.extras?.getString("action_url")
// Вариант 2: action_url передан как data Uri.
val dataUri = intent.data
val fromData = dataUri?.toString()
val actionUrl = fromExtras ?: fromData
if (actionUrl.isNullOrBlank()) return
android.util.Log.i("HMS", "MainActivity.handleIntentForActionUrl: $actionUrl")
// Проксируем URL во Flutter, где уже есть общая логика навигации по диплинкам.
methodChannel?.invokeMethod("openActionUrl", actionUrl)
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 12.0833V7.91666C0 7.56944 0.121528 7.2743 0.364583 7.03125C0.607639 6.78819 0.902778 6.66666 1.25 6.66666C1.59722 6.66666 1.89236 6.78819 2.13542 7.03125C2.37847 7.2743 2.5 7.56944 2.5 7.91666V12.0833C2.5 12.4306 2.37847 12.7257 2.13542 12.9687C1.89236 13.2118 1.59722 13.3333 1.25 13.3333C0.902778 13.3333 0.607639 13.2118 0.364583 12.9687C0.121528 12.7257 0 12.4306 0 12.0833ZM5 12.0833V5.83333C5 5.48611 5.12153 5.19097 5.36458 4.94791C5.60764 4.70486 5.90278 4.58333 6.25 4.58333C6.59722 4.58333 6.89236 4.70486 7.13542 4.94791C7.37847 5.19097 7.5 5.48611 7.5 5.83333V12.0833C7.5 12.4306 7.37847 12.7257 7.13542 12.9687C6.89236 13.2118 6.59722 13.3333 6.25 13.3333C5.90278 13.3333 5.60764 13.2118 5.36458 12.9687C5.12153 12.7257 5 12.4306 5 12.0833ZM10 12.0833V3.75C10 3.40278 10.1215 3.10764 10.3646 2.86458C10.6076 2.62153 10.9028 2.5 11.25 2.5C11.5972 2.5 11.8924 2.62153 12.1354 2.86458C12.3785 3.10764 12.5 3.40278 12.5 3.75V12.0833C12.5 12.4306 12.3785 12.7257 12.1354 12.9687C11.8924 13.2118 11.5972 13.3333 11.25 13.3333C10.9028 13.3333 10.6076 13.2118 10.3646 12.9687C10.1215 12.7257 10 12.4306 10 12.0833ZM15 6.8125V1.25C15 0.902777 15.1215 0.607639 15.3646 0.364583C15.6076 0.121528 15.9028 0 16.25 0C16.5972 0 16.8924 0.121528 17.1354 0.364583C17.3785 0.607639 17.5 0.902777 17.5 1.25V6.54166C17.3611 6.51389 17.2257 6.49305 17.0938 6.47916C16.9618 6.46528 16.8194 6.45833 16.6667 6.45833C16.375 6.45833 16.0868 6.48958 15.8021 6.55208C15.5174 6.61458 15.25 6.70139 15 6.8125ZM16.0729 13.0937C15.9132 12.934 15.8333 12.7361 15.8333 12.5C15.8333 12.2639 15.9132 12.066 16.0729 11.9062C16.2326 11.7465 16.4306 11.6667 16.6667 11.6667C16.9028 11.6667 17.1007 11.7465 17.2604 11.9062C17.4201 12.066 17.5 12.2639 17.5 12.5C17.5 12.7361 17.4201 12.934 17.2604 13.0937C17.1007 13.2535 16.9028 13.3333 16.6667 13.3333C16.4306 13.3333 16.2326 13.2535 16.0729 13.0937ZM15.8333 10V8.75C15.8333 8.51389 15.9132 8.31597 16.0729 8.15625C16.2326 7.99652 16.4306 7.91666 16.6667 7.91666C16.9028 7.91666 17.1007 7.99652 17.2604 8.15625C17.4201 8.31597 17.5 8.51389 17.5 8.75V10C17.5 10.2361 17.4201 10.434 17.2604 10.5937C17.1007 10.7535 16.9028 10.8333 16.6667 10.8333C16.4306 10.8333 16.2326 10.7535 16.0729 10.5937C15.9132 10.434 15.8333 10.2361 15.8333 10Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.292969 2.29289C0.683493 1.90237 1.31651 1.90237 1.70703 2.29289L3.41406 3.99992H11.4424C12.3865 3.99991 13.1558 3.99969 13.7803 4.05071C14.4257 4.10345 15.0042 4.21564 15.543 4.49016C16.3897 4.92159 17.0783 5.61023 17.5098 6.45696C17.7843 6.99573 17.8965 7.57424 17.9492 8.21965C17.9518 8.25175 17.9517 8.28444 17.9541 8.31731L22.4189 5.12981C23.0808 4.65704 24 5.12991 24 5.94328V18.0566C24 18.8699 23.0808 19.3428 22.4189 18.87L18 15.7138V18.5859L19.707 20.2929C20.0976 20.6834 20.0976 21.3164 19.707 21.707C19.3165 22.0975 18.6835 22.0975 18.293 21.707L0.292969 3.70696C-0.097555 3.31643 -0.0975551 2.68342 0.292969 2.29289ZM1 7.99895C1.55212 7.99895 1.99974 8.44689 2 8.99895V13.3993C2 14.3959 2.00036 15.0836 2.04395 15.6171C2.08658 16.1387 2.16554 16.4249 2.27246 16.6347C2.51215 17.105 2.89491 17.4868 3.36523 17.7265C3.57511 17.8334 3.86102 17.9124 4.38281 17.955C4.91619 17.9986 5.60333 17.9989 6.59961 17.9989H11C11.5521 17.9989 11.9997 18.4469 12 18.9989C12 19.5512 11.5523 19.9989 11 19.9989H6.55762C5.61342 19.999 4.84421 19.9992 4.21973 19.9482C3.57428 19.8954 2.99583 19.7832 2.45703 19.5087C1.61035 19.0773 0.921646 18.3886 0.490235 17.5419C0.215871 17.0032 0.103504 16.4245 0.0507818 15.7792C-0.000207456 15.1548 -1.10191e-05 14.3854 5.0477e-07 13.4413V8.99895C0.00026378 8.44689 0.447878 7.99895 1 7.99895ZM16 16.5859V10.5995C16 9.60324 15.9986 8.91611 15.9551 8.38274C15.9125 7.86113 15.8344 7.57502 15.7275 7.36516C15.4879 6.89475 15.1052 6.51207 14.6348 6.27239C14.4249 6.16547 14.1389 6.0865 13.6172 6.04387C13.0837 6.00029 12.396 5.99992 11.3994 5.99992H5.41406L16 16.5859Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.83398 9.03418C6.1464 8.72177 6.6534 8.72178 6.96582 9.03418C7.27824 9.3466 7.27824 9.8536 6.96582 10.166L2.73047 14.4004H5.59961C6.04144 14.4004 6.40039 14.7584 6.40039 15.2002C6.40032 15.642 6.04139 16 5.59961 16H0.799805C0.750353 16 0.702243 15.994 0.655273 15.9854C0.650431 15.9845 0.645453 15.9844 0.640625 15.9834C0.632394 15.9817 0.624348 15.9795 0.616211 15.9775C0.599845 15.9737 0.583526 15.9697 0.567383 15.9648C0.525709 15.9522 0.485753 15.936 0.447266 15.917C0.370564 15.8792 0.298188 15.8294 0.234375 15.7656C0.170233 15.7015 0.119834 15.6289 0.0820312 15.5518C0.0522722 15.4911 0.0292924 15.4265 0.015625 15.3584C0.0146554 15.3536 0.0145519 15.3486 0.0136719 15.3438C0.0052094 15.2971 7.59671e-06 15.2493 0 15.2002V10.4004C0 9.95863 0.358067 9.59972 0.799805 9.59961C1.24163 9.59961 1.59961 9.95856 1.59961 10.4004V13.2686L5.83398 9.03418ZM15.2002 0C15.2493 1.2747e-05 15.2971 0.0052045 15.3438 0.0136719C15.3486 0.0145519 15.3536 0.0146554 15.3584 0.015625C15.3666 0.017277 15.3747 0.0195822 15.3828 0.0214844C15.3992 0.0253083 15.4155 0.0293148 15.4316 0.0341797C15.4733 0.0467652 15.5133 0.0631386 15.5518 0.0820312C15.6289 0.119834 15.7015 0.170233 15.7656 0.234375C15.8294 0.298188 15.8792 0.370564 15.917 0.447266C15.9252 0.463964 15.9334 0.480716 15.9404 0.498047C15.9612 0.549032 15.9755 0.601773 15.9854 0.655273C15.994 0.702239 16 0.750358 16 0.799805V5.59961C16 6.04137 15.6419 6.40028 15.2002 6.40039C14.7584 6.40039 14.4004 6.04144 14.4004 5.59961V2.73047L10.166 6.96582C9.8536 7.27824 9.3466 7.27824 9.03418 6.96582C8.72178 6.6534 8.72177 6.1464 9.03418 5.83398L13.2695 1.59961H10.4004C9.95856 1.59961 9.59961 1.24163 9.59961 0.799805C9.59972 0.358067 9.95863 0 10.4004 0H15.2002Z" fill="#EFF0FA"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 6.3V6C16 3.79 14.21 2 12 2C9.79 2 8 3.79 8 6V11" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.04004 14.19C9.77004 15 10.83 15.5 12 15.5C14.21 15.5 16 13.71 16 11.5V11" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.77979 16.9499C8.14979 18.2199 9.97978 18.9999 11.9998 18.9999C16.2198 18.9999 19.6498 15.5699 19.6498 11.3499V9.6499" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.3501 9.6499V11.3499C4.3501 12.4099 4.5601 13.4099 4.9501 14.3299" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.0702 2.83984L3.93018 18.9898" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 3V6" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 19V22" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15.5C14.21 15.5 16 13.71 16 11.5V6C16 3.79 14.21 2 12 2C9.79 2 8 3.79 8 6V11.5C8 13.71 9.79 15.5 12 15.5Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.35059 9.6499V11.3499C4.35059 15.5699 7.78059 18.9999 12.0006 18.9999C16.2206 18.9999 19.6506 15.5699 19.6506 11.3499V9.6499" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 19V22" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.40023 8.79965C6.44894 8.79967 6.49651 8.80496 6.54281 8.81332C6.54796 8.81425 6.5533 8.81424 6.55844 8.81527C6.56666 8.81692 6.57472 8.81923 6.58285 8.82113C6.5992 8.82496 6.61555 8.82896 6.63168 8.83383C6.64056 8.83651 6.64929 8.83963 6.65805 8.84262C6.69881 8.85648 6.73865 8.87374 6.77719 8.89437C6.80436 8.90894 6.83012 8.9257 6.85531 8.9432C6.89387 8.97001 6.93129 8.99965 6.96566 9.03402C6.9997 9.06805 7.02889 9.10526 7.05551 9.1434C7.05897 9.14836 7.06289 9.153 7.06625 9.15804C7.12722 9.24979 7.16631 9.35128 7.18539 9.4559C7.19389 9.50255 7.20001 9.55035 7.20004 9.59945V14.4002C7.19984 14.8418 6.84178 15.1998 6.40023 15.2C5.95852 15.2 5.59964 14.8419 5.59945 14.4002V11.5311L1.36508 15.7655C1.05264 16.0777 0.546577 16.0778 0.234217 15.7655C-0.0781257 15.4531 -0.0780189 14.947 0.234217 14.6346L4.46762 10.4002H1.59945C1.15782 10.4 0.799647 10.0411 0.799647 9.59945C0.799876 9.15796 1.15796 8.79988 1.59945 8.79965H6.40023ZM14.6346 0.234217C14.947 -0.0780189 15.4531 -0.0781257 15.7655 0.234217C16.0778 0.546577 16.0777 1.05264 15.7655 1.36508L11.5321 5.59945H14.4002C14.8419 5.59964 15.2 5.95852 15.2 6.40023C15.1998 6.84178 14.8418 7.19984 14.4002 7.20004H9.59945C9.55035 7.20001 9.50255 7.19389 9.4559 7.18539C9.45074 7.18445 9.44541 7.18448 9.44027 7.18344C9.43204 7.18177 9.42399 7.17949 9.41586 7.17758C9.3995 7.17372 9.38317 7.16978 9.36703 7.16488C9.35943 7.16257 9.35207 7.1596 9.34457 7.15707C9.30221 7.14282 9.2605 7.12587 9.22054 7.10433C9.19365 7.08982 9.16833 7.0729 9.1434 7.05551C9.10526 7.02889 9.06805 6.9997 9.03402 6.96566C8.99965 6.93129 8.97001 6.89387 8.9432 6.85531C8.9257 6.83012 8.90894 6.80436 8.89437 6.77719C8.85465 6.70298 8.82796 6.62378 8.81332 6.54281C8.80496 6.49651 8.79967 6.44894 8.79965 6.40023V1.59945C8.79988 1.15796 9.15796 0.799876 9.59945 0.799647C10.0411 0.799647 10.4 1.15782 10.4002 1.59945V4.46762L14.6346 0.234217Z" fill="#EFF0FA"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 0C15.523 0 20 4.477 20 10C20 15.523 15.523 20 10 20C4.477 20 0 15.523 0 10C0 4.477 4.477 0 10 0ZM10 2C7.87827 2 5.84344 2.84285 4.34315 4.34315C2.84285 5.84344 2 7.87827 2 10C2 12.1217 2.84285 14.1566 4.34315 15.6569C5.84344 17.1571 7.87827 18 10 18C12.1217 18 14.1566 17.1571 15.6569 15.6569C17.1571 14.1566 18 12.1217 18 10C18 7.87827 17.1571 5.84344 15.6569 4.34315C14.1566 2.84285 12.1217 2 10 2ZM10 13C10.2652 13 10.5196 13.1054 10.7071 13.2929C10.8946 13.4804 11 13.7348 11 14C11 14.2652 10.8946 14.5196 10.7071 14.7071C10.5196 14.8946 10.2652 15 10 15C9.73478 15 9.48043 14.8946 9.29289 14.7071C9.10536 14.5196 9 14.2652 9 14C9 13.7348 9.10536 13.4804 9.29289 13.2929C9.48043 13.1054 9.73478 13 10 13ZM10 4C10.2652 4 10.5196 4.10536 10.7071 4.29289C10.8946 4.48043 11 4.73478 11 5V11C11 11.2652 10.8946 11.5196 10.7071 11.7071C10.5196 11.8946 10.2652 12 10 12C9.73478 12 9.48043 11.8946 9.29289 11.7071C9.10536 11.5196 9 11.2652 9 11V5C9 4.73478 9.10536 4.48043 9.29289 4.29289C9.48043 4.10536 9.73478 4 10 4Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -35,6 +35,8 @@
<string>This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us.</string> <string>This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string> <string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
<key>NSMicrophoneUsageDescription</key>
<string>Нужен для звука в видеозвонках с консультантом</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string></string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'dart:io' as io; import 'dart:io' as io;
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/firebase_options.dart'; import 'package:eatvio_app/firebase_options.dart';
@@ -16,6 +17,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
/// Класс для инициализации приложения /// Класс для инициализации приложения
class AppInitializer { class AppInitializer {
/// Инициализация всех сервисов приложения /// Инициализация всех сервисов приложения
static Future<void> initialize({required AppBuildType buildType}) async { static Future<void> initialize({required AppBuildType buildType}) async {
await _initializeEnvironment(); await _initializeEnvironment();
@@ -79,11 +81,12 @@ class AppInitializer {
options.dsn = EnvConfig.sentryDsn; options.dsn = EnvConfig.sentryDsn;
options.tracesSampleRate = 1.0; options.tracesSampleRate = 1.0;
options.debug = false; options.debug = false;
options.beforeSend = (event, hint) {
if (kDebugMode) return null;
if (!ErrorReportPolicy.allowSentryEvent(event, hint)) return null;
return event;
};
}, appRunner: appRunner); }, appRunner: appRunner);
FlutterError.onError = (details) async {
if (kDebugMode) return;
await Sentry.captureException(details.exception, stackTrace: details.stack);
};
} }
} }

View File

@@ -1,4 +1,5 @@
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/routing/auth_routes.dart'; import 'package:eatvio_app/core/routing/auth_routes.dart';
import 'package:eatvio_app/core/routing/content_routes.dart'; import 'package:eatvio_app/core/routing/content_routes.dart';
import 'package:eatvio_app/core/routing/main_navigation_routes.dart'; import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
@@ -6,6 +7,7 @@ import 'package:eatvio_app/core/routing/other_routes.dart';
import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/routing/settings_routes.dart'; import 'package:eatvio_app/core/routing/settings_routes.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -14,8 +16,8 @@ import 'package:go_router/go_router.dart';
class AppRouter { class AppRouter {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Домены для deep links static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com', 'dev2.eatvio.com'];
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru']; static const List<String> metaSegments = ['ru', 'en', 'api', 'v2'];
static GoRouter appRouter = GoRouter( static GoRouter appRouter = GoRouter(
initialLocation: RouteConstants.updateScreen, initialLocation: RouteConstants.updateScreen,
@@ -30,129 +32,212 @@ class AppRouter {
], ],
); );
/// Обработка редиректов для deep links static String? _resolveRecipeDeepLink(List<String> effectiveSegments) {
/// Возвращает null для обычной навигации, чтобы не мешать внутренним переходам if (effectiveSegments.length != 2) return null;
static String? _handleRedirect(BuildContext context, GoRouterState state) { final contentIdentifier = effectiveSegments[1];
final uri = state.uri; developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
return '/about-recipes-screen?slug=$contentIdentifier';
}
static String? _resolveBlogPostDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
return '/blog-post-screen?id=$contentIdentifier';
}
static String? _resolvePlanDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
final planId = int.tryParse(contentIdentifier);
if (planId != null) {
developer.log('Deep link: diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
return '/about-plan-screen?slug=$contentIdentifier';
}
static String? _resolveDietDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
final dietId = int.tryParse(contentIdentifier);
if (dietId != null) {
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
return '/about-diet-screen?id=$dietId';
}
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
return '/about-diet-screen?slug=$contentIdentifier';
}
const dietTabIndexWithHiddenPlansTab = 3;
const dietTabIndexWithoutHiddenPlansTab = 4;
final initialTabIndex = hidePlans ? dietTabIndexWithHiddenPlansTab : dietTabIndexWithoutHiddenPlansTab;
return '${RouteConstants.feedScreen}?initialTabIndex=$initialTabIndex';
}
static String? _resolveChatDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
} else if (effectiveSegments.length == 3 && effectiveSegments[1] == 'show') {
final contentIdentifier = effectiveSegments[2];
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
return '${RouteConstants.chatScreen}?uuid=$contentIdentifier';
}
developer.log('Deep link: chat', name: 'AppRouter');
return RouteConstants.chatSelectScreen;
}
static String? _resolveProfileDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length == 2) {
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
return '/profile-screen?slug=$contentIdentifier';
}
return RouteConstants.myProfileScreen;
}
static String? _resolveBadgeDeepLink(List<String> effectiveSegments) {
return RouteConstants.myProfileScreen;
}
static String? _resolveDiaryDeepLink(List<String> effectiveSegments) {
developer.log('Deep link: diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
static String? _resolveConsultantDiaryCommentDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 2) return null;
final contentIdentifier = effectiveSegments[1];
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
return RouteConstants.diaryScreen;
}
static String? _resolveConsultationsDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 3 || effectiveSegments[1] != 'status') return RouteConstants.consultationsScreen;
final consultationId = effectiveSegments[2];
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
return '${InfoByOrderScreen.path}?planId=$consultationId';
}
static String? _resolveDeepLinkUri(Uri uri) {
final host = uri.host.toLowerCase(); final host = uri.host.toLowerCase();
final segments = uri.pathSegments; final segments = uri.pathSegments;
// Проверяем, является ли это deep link по домену final isDeepLink = deepLinkHosts.any((allowedHost) => host == allowedHost || host.endsWith('.$allowedHost'));
final isDeepLink = _deepLinkHosts.contains(host);
// Если это не deep link, не мешаем обычной навигации
if (!isDeepLink) { if (!isDeepLink) {
return null; return null;
} }
// Логирование для отладки developer.log('Deep link detected: host=$host, path=${uri.path}, segments=$segments', name: 'AppRouter');
developer.log(
'Deep link detected: host=$host, path=${uri.path}, segments=$segments',
name: 'AppRouter',
);
// Проверка на пустой путь или некорректный формат
if (segments.isEmpty) { if (segments.isEmpty) {
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter'); developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.feedScreen; return RouteConstants.feedScreen;
} }
// Проверка языка (первый сегмент должен быть ru или en) final effectiveSegments = segments.where((segment) => !metaSegments.contains(segment)).toList();
if (segments[0] != 'ru' && segments[0] != 'en') { if (effectiveSegments.isEmpty) {
developer.log( developer.log('No effective segments after language check, redirecting to feed', name: 'AppRouter');
'Invalid language segment: ${segments[0]}, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen; return RouteConstants.feedScreen;
} }
// Обработка deep links с учетом языка final type = effectiveSegments[0];
// Формат: /{lang}/{type}/{identifier?}
if (segments.length == 4) {
// Формат: /{lang}/diary/{...}
final type = segments[1];
if (type == 'diary') {
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.diaryScreen;
}
developer.log(
'Unknown deep link type for 4 segments: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
} else if (segments.length == 3) {
// Формат: /{lang}/{type}/{identifier}
final type = segments[1];
final contentIdentifier = segments[2];
switch (type) { switch (type) {
case 'recipe': case 'recipe':
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter'); final target = _resolveRecipeDeepLink(effectiveSegments);
FlutterNativeSplash.remove(); if (target != null) return target;
return '/about-recipes-screen?slug=$contentIdentifier'; break;
case 'blog_post': case 'blog_post':
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter'); final target = _resolveBlogPostDeepLink(effectiveSegments);
FlutterNativeSplash.remove(); if (target != null) return target;
return '/blog-post-screen?id=$contentIdentifier'; break;
case 'plan': case 'plan':
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter'); final target = _resolvePlanDeepLink(effectiveSegments);
FlutterNativeSplash.remove(); if (target != null) return target;
return '/about-plan-screen?slug=$contentIdentifier'; break;
case 'diet': case 'diet':
developer.log('Deep link: diet/$contentIdentifier', name: 'AppRouter'); final target = _resolveDietDeepLink(effectiveSegments);
FlutterNativeSplash.remove(); if (target != null) return target;
return '/about-diet-screen?slug=$contentIdentifier'; break;
case 'chat': case 'chat':
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter'); final target = _resolveChatDeepLink(effectiveSegments);
FlutterNativeSplash.remove(); if (target != null) return target;
return RouteConstants.chatSelectScreen; break;
case 'profile': case 'profile':
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter'); final target = _resolveProfileDeepLink(effectiveSegments);
FlutterNativeSplash.remove(); if (target != null) return target;
return '/profile-screen?slug=$contentIdentifier'; break;
case 'diary': case 'badge':
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter'); final target = _resolveBadgeDeepLink(effectiveSegments);
FlutterNativeSplash.remove(); if (target != null) return target;
return RouteConstants.diaryScreen; break;
default: case 'diary':
developer.log( final target = _resolveDiaryDeepLink(effectiveSegments);
'Unknown deep link type: $type, redirecting to feed', if (target != null) return target;
name: 'AppRouter', break;
); case 'consultant_diary_comment':
FlutterNativeSplash.remove(); final target = _resolveConsultantDiaryCommentDeepLink(effectiveSegments);
return RouteConstants.feedScreen; if (target != null) return target;
} break;
} else if (segments.length == 2) { case 'consultations':
// Формат: /{lang}/{type} - только для chat final target = _resolveConsultationsDeepLink(effectiveSegments);
final type = segments[1]; if (target != null) return target;
break;
if (type == 'chat') {
developer.log('Deep link: chat', name: 'AppRouter');
FlutterNativeSplash.remove();
return RouteConstants.chatSelectScreen;
} else {
developer.log(
'Unknown deep link type for 2 segments: $type, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
}
} else {
// Неизвестный формат deep link
developer.log(
'Unknown deep link format: ${segments.length} segments, redirecting to feed',
name: 'AppRouter',
);
FlutterNativeSplash.remove();
return RouteConstants.feedScreen;
} }
developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter');
return RouteConstants.feedScreen;
}
static String? _handleRedirect(BuildContext context, GoRouterState state) {
return _resolveDeepLinkUri(state.uri);
}
static void navigateByDeepLinkUrl(String url) {
if (url.isEmpty) {
developer.log('navigateByDeepLinkUrl: empty url, redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
return;
}
Uri? uri;
try {
uri = Uri.tryParse(url);
} catch (e) {
developer.log('navigateByDeepLinkUrl: failed to parse url=$url, error=$e, redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
return;
}
if (uri == null) {
developer.log('navigateByDeepLinkUrl: uri is null for url=$url, redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
return;
}
if (uri.host.isEmpty && uri.pathSegments.isNotEmpty) {
uri = Uri(
scheme: 'https',
host: deepLinkHosts.first,
pathSegments: uri.pathSegments,
queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters,
);
}
final target = _resolveDeepLinkUri(uri);
FlutterNativeSplash.remove();
if (target != null) {
appRouter.go(target);
return;
}
developer.log('navigateByDeepLinkUrl: unresolved url=$url (normalized=${uri.toString()}), redirecting to feed', name: 'AppRouter');
appRouter.go(RouteConstants.feedScreen);
} }
static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) {

View File

@@ -5,15 +5,13 @@ import 'package:eatvio_app/core/comments/repositories/comments_repository.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CommentsCubit extends Cubit<CommentsState> { class CommentsCubit extends Cubit<CommentsState> {
final FeedRepository feedRepository; final FeedRepository feedRepository;
final CommentsRepository commentsRepository; final CommentsRepository commentsRepository;
CommentsCubit({ CommentsCubit({required this.feedRepository, required this.commentsRepository}) : super(const CommentsState());
required this.feedRepository,
required this.commentsRepository,
}) : super(const CommentsState());
void setReplyId(int? replyId) { void setReplyId(int? replyId) {
emit((state as CommentsStateLoaded).copyWith(replyId: replyId)); emit((state as CommentsStateLoaded).copyWith(replyId: replyId));
@@ -36,51 +34,34 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id))); emit(CommentsState.loaded(comment: await feedRepository.getPlansComments(slug: id)));
break; break;
} }
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.error(errorMessage: e.toString())); emit(CommentsState.error(errorMessage: e.toString()));
} }
} }
Future<void> sendComment({ Future<bool> sendComment({required String id, required String commentText, required CommentSourceType commentSourceType, int? parentCommentId}) async {
required String id,
required String commentText,
required CommentSourceType commentSourceType,
int? parentCommentId,
}) async {
try { try {
switch (commentSourceType) { switch (commentSourceType) {
case CommentSourceType.blogPost: case CommentSourceType.blogPost:
await feedRepository.sendBlogPostComment( await feedRepository.sendBlogPostComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break; break;
case CommentSourceType.diet: case CommentSourceType.diet:
await feedRepository.sendDietsComment( await feedRepository.sendDietsComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break; break;
case CommentSourceType.recipe: case CommentSourceType.recipe:
await feedRepository.sendRecipesComment( await feedRepository.sendRecipesComment(id: id, commentText: commentText, parentCommentId: parentCommentId);
id: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break; break;
case CommentSourceType.plan: case CommentSourceType.plan:
await feedRepository.sendPlansComment( await feedRepository.sendPlansComment(slug: id, commentText: commentText, parentCommentId: parentCommentId);
slug: id,
commentText: commentText,
parentCommentId: parentCommentId,
);
break; break;
} }
getComments(id, commentSourceType); getComments(id, commentSourceType);
} catch (e) { return true;
} catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.error(errorMessage: e.toString())); emit(CommentsState.error(errorMessage: e.toString()));
return false;
} }
} }
@@ -93,6 +74,7 @@ class CommentsCubit extends Cubit<CommentsState> {
emit(CommentsState.loaded(comment: updatedCommentModel)); emit(CommentsState.loaded(comment: updatedCommentModel));
return true; return true;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.loaded(comment: commentModel)); emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
@@ -101,13 +83,15 @@ class CommentsCubit extends Cubit<CommentsState> {
Future<bool> updateComment({required int id, required String body}) async { Future<bool> updateComment({required int id, required String body}) async {
final commentModel = (state as CommentsStateLoaded).comment; final commentModel = (state as CommentsStateLoaded).comment;
final updatedCommentModel = final updatedCommentModel = commentModel.copyWith(
commentModel.copyWith(data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList()); data: commentModel.data.map((comment) => comment.id == id ? comment.copyWith(body: body) : comment).toList(),
);
try { try {
await commentsRepository.updateComment(id: id, body: body); await commentsRepository.updateComment(id: id, body: body);
emit(CommentsState.loaded(comment: updatedCommentModel)); emit(CommentsState.loaded(comment: updatedCommentModel));
return true; return true;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
emit(CommentsState.loaded(comment: commentModel)); emit(CommentsState.loaded(comment: commentModel));
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/comments/models/comment_update_model.dart'; import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
import 'package:eatvio_app/core/comments/services/comments_service.dart'; import 'package:eatvio_app/core/comments/services/comments_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CommentsRepository { class CommentsRepository {
final CommentsService service; final CommentsService service;
@@ -8,20 +7,12 @@ class CommentsRepository {
CommentsRepository({required this.service}); CommentsRepository({required this.service});
Future<void> deleteComment({required int id}) async { Future<void> deleteComment({required int id}) async {
try { return service.deleteComment(id);
return service.deleteComment(id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> updateComment({required int id, required String body}) async { Future<void> updateComment({required int id, required String body}) async {
try { return service.updateComment(id, CommentUpdateModel(body: body));
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart';
import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class MarketingCubit extends Cubit<MarketingCubitState> { class MarketingCubit extends Cubit<MarketingCubitState> {
final Prefs prefs; final Prefs prefs;
@@ -22,8 +23,9 @@ class MarketingCubit extends Cubit<MarketingCubitState> {
await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true); await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true);
} }
return needToShowMarketingBottomSheet; return needToShowMarketingBottomSheet;
} catch (e) { } catch (e, st) {
rethrow; await reportFrom(e, st, this);
return false;
} }
} }
} }

View File

@@ -1,10 +1,13 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/constants/app_constants.dart'; import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/error_reporting/sentry_error_reporter.dart';
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/prefs.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:sentry_dio/sentry_dio.dart'; import 'package:sentry_dio/sentry_dio.dart';
@@ -14,6 +17,7 @@ class ExternalRegistrations {
/// Регистрация всех внешних зависимостей /// Регистрация всех внешних зависимостей
static void registerAll() { static void registerAll() {
sl.registerLazySingleton<ErrorReporter>(SentryErrorReporter.new);
_registerDio(); _registerDio();
_registerPrefs(); _registerPrefs();
} }
@@ -30,7 +34,9 @@ class ExternalRegistrations {
sendTimeout: AppConstants.pendingDelay, sendTimeout: AppConstants.pendingDelay,
), ),
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]) )..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
..addSentry(), ..addSentry(
failedRequestStatusCodes: const [SentryStatusCode.defaultRange()],
),
); );
} }

View File

@@ -33,6 +33,7 @@ import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.da
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart'; import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/video_calls/repositories/video_call_repository.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
/// Регистрация всех Repository в системе внедрения зависимостей /// Регистрация всех Repository в системе внедрения зависимостей
@@ -49,6 +50,7 @@ class RepositoryRegistrations {
static void _registerSingletonRepositories() { static void _registerSingletonRepositories() {
// Core repositories // Core repositories
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl())); sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
sl.registerLazySingleton<VideoCallRepository>(() => VideoCallRepository(videoCallService: sl()));
sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl())); sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl()));
sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl())); sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl()));
sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl())); sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl()));

View File

@@ -35,6 +35,7 @@ import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart'; import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_service.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
/// Регистрация всех Service в системе внедрения зависимостей /// Регистрация всех Service в системе внедрения зависимостей
@@ -50,6 +51,7 @@ class ServiceRegistrations {
static void _registerSingletonServices() { static void _registerSingletonServices() {
// Core services // Core services
sl.registerLazySingleton<ChatService>(() => ChatService(sl())); sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
sl.registerLazySingleton<VideoCallService>(() => VideoCallService(sl()));
sl.registerLazySingleton<FeedService>(() => FeedService(sl())); sl.registerLazySingleton<FeedService>(() => FeedService(sl()));
sl.registerLazySingleton<ProfileService>(() => ProfileService(sl())); sl.registerLazySingleton<ProfileService>(() => ProfileService(sl()));
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl())); sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
@@ -72,7 +74,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl())); sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl())); sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl())); sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi()); sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi(errorReporter: sl()));
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl())); sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl())); sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl())); sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));

View File

@@ -0,0 +1,80 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// Rules for which errors are sent to crash reporting.
///
/// [shouldReport] is used by [ErrorReporter]. [allowSentryEvent] is used in
/// [SentryFlutter.init] as a last-line filter (Dio failed-request capture, etc.).
class ErrorReportPolicy {
ErrorReportPolicy._();
/// When `false`, the error is not sent via [ErrorReporter].
static bool shouldReport(Object? error) {
if (error == null) return false;
if (error is String) return false;
if (error is SocketException) return false;
if (error is DioException) {
return _shouldReportDio(error);
}
return true;
}
/// Used in SDK [SentryOptions.beforeSend]. Return `false` to drop the event.
///
/// For [DioException], rules differ from [shouldReport]: events from `sentry_dio`
/// (e.g. 5xx) must pass through here, while [shouldReport] returns false for 5xx
/// so [SentryErrorReporter] does not duplicate them after `catch` in cubits / manual [report] calls.
static bool allowSentryEvent(SentryEvent event, Hint hint) {
final t = event.throwable;
if (t == null) return true;
if (t is DioException) {
return _allowSentrySdkDio(t);
}
return shouldReport(t);
}
/// Cancel, timeouts, socket / connection errors — do not send (noisy, often user environment).
static bool _isTransientDioFailure(DioException e) {
switch (e.type) {
case DioExceptionType.cancel:
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.connectionError:
return true;
case DioExceptionType.unknown:
case DioExceptionType.badResponse:
case DioExceptionType.badCertificate:
break;
}
return e.error is SocketException;
}
/// Noise to drop for any Dio-originated SDK event; do not use for manual [report].
static bool _allowSentrySdkDio(DioException e) {
if (_isTransientDioFailure(e)) return false;
final code = e.response?.statusCode;
if (code != null && code >= 400 && code < 500) return false;
return true;
}
static bool _shouldReportDio(DioException e) {
if (_isTransientDioFailure(e)) return false;
final code = e.response?.statusCode;
if (code != null) {
if (code >= 500 && code <= 599) {
return false;
}
if (code >= 400 && code < 500) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,9 @@
/// Application-wide error reporting (e.g. Sentry).
abstract class ErrorReporter {
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
});
}

View File

@@ -0,0 +1,14 @@
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
/// Reporting sink that does nothing (dev tooling, tests, diagnostics).
class NoOpErrorReporter implements ErrorReporter {
const NoOpErrorReporter();
@override
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
}) async {}
}

View File

@@ -0,0 +1,17 @@
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/error_reporting/no_op_error_reporter.dart';
import 'package:eatvio_app/core/locator_service.dart';
/// Отправка в [ErrorReporter] с тегом origin = [context.runtimeType] (имя кубита/виджета и т.д.).
///
/// Если [ErrorReporter] не зарегистрирован (например, после [GetIt.reset] в тестах), используется [NoOpErrorReporter].
Future<void> reportFrom(Object error, StackTrace? stackTrace, Object context) {
final reporter = sl.isRegistered<ErrorReporter>()
? sl<ErrorReporter>()
: const NoOpErrorReporter();
return reporter.report(
error,
stackTrace,
origin: context.runtimeType.toString(),
);
}

View File

@@ -0,0 +1,33 @@
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class SentryErrorReporter implements ErrorReporter {
SentryErrorReporter();
@override
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? extras,
}) async {
if (!ErrorReportPolicy.shouldReport(error)) return;
await Sentry.captureException(
error,
stackTrace: stackTrace,
withScope: (scope) {
if (origin != null && origin.isNotEmpty) {
scope.setTag('origin', origin);
}
if (extras != null && extras.isNotEmpty) {
scope.setContexts(
'report_extras',
extras.map((k, v) => MapEntry(k, v?.toString())),
);
}
},
);
}
}

View File

@@ -5,18 +5,20 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart' show MethodChannel, PlatformException; import 'package:flutter/services.dart' show MethodChannel, PlatformException;
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
class FirebaseApi { class FirebaseApi {
final ErrorReporter _errorReporter;
final _firebaseMessaging = FirebaseMessaging.instance; final _firebaseMessaging = FirebaseMessaging.instance;
FirebaseApi({required ErrorReporter errorReporter}) : _errorReporter = errorReporter;
Future<String?> getToken() async { Future<String?> getToken() async {
String? token; String? token;
@@ -62,7 +64,7 @@ class FirebaseApi {
); );
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция // Не пробрасываем ошибку дальше, так как это не критичная операция
} }
} }
@@ -87,7 +89,7 @@ class FirebaseApi {
); );
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция // Не пробрасываем ошибку дальше, так как это не критичная операция
} }
} }
@@ -113,12 +115,12 @@ class FirebaseApi {
} on PlatformException catch (e, st) { } on PlatformException catch (e, st) {
showErrorToast('$e $st'); showErrorToast('$e $st');
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null; return null;
} catch (e, st) { } catch (e, st) {
showErrorToast('$e $st'); showErrorToast('$e $st');
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null; return null;
} }
} }

View File

@@ -3,7 +3,6 @@ import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
abstract class ImageCompressUtility { abstract class ImageCompressUtility {
@@ -30,44 +29,21 @@ abstract class ImageCompressUtility {
} }
static Future<File> compressFile(File file) async { static Future<File> compressFile(File file) async {
// Skip compression in test environment to avoid hangs
try { try {
final appConfig = sl<AppConfig>(); final appConfig = sl<AppConfig>();
if (appConfig.buildType == AppBuildType.tests) { if (appConfig.buildType == AppBuildType.tests) {
// In tests, return the original file without compression
return file; return file;
} }
} catch (_) { } catch (_) {}
// If AppConfig is not available, proceed with compression
final compressedPath = '${p.withoutExtension(file.path)}_compressed.jpg';
final result = await FlutterImageCompress.compressAndGetFile(file.absolute.path, compressedPath, quality: 90, minHeight: 1080, minWidth: 1080);
if (result == null) {
return file;
} }
final image = img.decodeImage(file.readAsBytesSync())!; return File(result.path);
int width = image.width;
int height = image.height;
if (width > height) {
if (width > 1080) {
height = (height * 1080) ~/ width;
width = 1080;
}
} else {
if (height > 1080) {
width = (width * 1080) ~/ height;
height = 1080;
}
}
String filePathWithoutExtension = p.withoutExtension(file.path);
String compressedFilePath = '${filePathWithoutExtension}_compressed.jpg';
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
compressedFilePath,
quality: 90,
minHeight: height,
minWidth: width,
);
return File(result!.path);
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -12,18 +13,21 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
final data = err.response?.data; final data = err.response?.data;
if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') { if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') {
final String phone = await sl<ProfileRepository>().getMyPhone(); final String phone = await sl<ProfileRepository>().getMyPhone();
final String passKey = await sl<ProfileRepository>().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone}); final String passKey = await sl<ProfileRepository>().getPassKey(routeName: 'user_detailz.change-phone-verify', routeParams: {"phone": phone});
await showErrorDialog(AppRouter.navigatorKey.currentContext!, await showErrorDialog(
title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона'); AppRouter.navigatorKey.currentContext!,
title: 'Привяжите номер телефона',
description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона',
);
final currentContext = AppRouter.navigatorKey.currentContext;
if (currentContext != null && currentContext.mounted && GoRouter.of(currentContext).state.path == RouteConstants.stepperBecomeConsultantScreen) {
GoRouter.of(currentContext).pop();
}
AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey); AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey);
return handler.reject( return handler.reject(
DioException( DioException(requestOptions: err.requestOptions, response: err.response, error: 'Phone number not verified', type: DioExceptionType.badResponse),
requestOptions: err.requestOptions,
response: err.response,
error: 'Phone number not verified',
type: DioExceptionType.badResponse,
),
); );
} }
} }

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
class InternetConnectionCubit extends Cubit<InternetConnectionState> { class InternetConnectionCubit extends Cubit<InternetConnectionState> {
@@ -33,7 +34,8 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
try { try {
result = await _connectivity.checkConnectivity(); result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) { } on PlatformException catch (e, st) {
await reportFrom(e, st, this);
log('Couldn\'t check connectivity status', error: e); log('Couldn\'t check connectivity status', error: e);
return; return;
} }
@@ -42,9 +44,13 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
} }
Future<void> _updateAccessStatus(List<ConnectivityResult> result) async { Future<void> _updateAccessStatus(List<ConnectivityResult> result) async {
final hasInternet = await _checkInternetAccess(result); try {
if (!hasInternet) showInternetConnectionToast(); final hasInternet = await _checkInternetAccess(result);
emit(state.copyWith(isInternetAvailable: hasInternet)); if (!hasInternet) showInternetConnectionToast();
emit(state.copyWith(isInternetAvailable: hasInternet));
} catch (e, st) {
await reportFrom(e, st, this);
}
} }
Future<bool> _checkInternetAccess(List<ConnectivityResult> result) async { Future<bool> _checkInternetAccess(List<ConnectivityResult> result) async {
@@ -57,7 +63,8 @@ class InternetConnectionCubit extends Cubit<InternetConnectionState> {
try { try {
final response = await http.get(uri).timeout(const Duration(seconds: 3)); final response = await http.get(uri).timeout(const Duration(seconds: 3));
return response.statusCode >= 100 && response.statusCode < 600; return response.statusCode >= 100 && response.statusCode < 600;
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
continue; continue;
} }
} }

View File

@@ -32,5 +32,5 @@ Future<void> init({required AppBuildType buildType}) async {
CubitRegistrations.registerAll(); CubitRegistrations.registerAll();
// Регистрация WebSocket менеджера // Регистрация WebSocket менеджера
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl())); sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl(), errorReporter: sl()));
} }

View File

@@ -0,0 +1,64 @@
import 'dart:developer' as developer;
import 'package:eatvio_app/core/app_router.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class PushNavigationHandler {
static bool _initialized = false;
static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push');
static Future<void> initialize() async {
if (_initialized) return;
_initialized = true;
try {
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleMessageInternal(initialMessage, source: 'initialMessage');
});
}
} catch (e) {
developer.log('PushNavigationHandler: error while getting initialMessage: $e', name: 'PushNavigationHandler');
}
FirebaseMessaging.onMessageOpenedApp.listen((message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'));
_hmsChannel.setMethodCallHandler((call) async {
if (call.method == 'openActionUrl') {
final url = call.arguments as String? ?? '';
handleActionUrlFromNative(url);
}
});
}
static void handleActionUrlFromNative(String url) {
if (url.isEmpty) {
developer.log('PushNavigationHandler.handleActionUrlFromNative: empty url', name: 'PushNavigationHandler');
return;
}
developer.log('PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(url);
}
static void handleMessage(RemoteMessage message) {
_handleMessageInternal(message, source: 'manualHandle');
}
static void _handleMessageInternal(RemoteMessage message, {required String source}) {
final data = message.data;
final actionUrl = data['action_url'] as String?;
if (actionUrl == null || actionUrl.isEmpty) {
developer.log('PushNavigationHandler: no action_url in message data, source=$source, data=$data', name: 'PushNavigationHandler');
return;
}
developer.log('PushNavigationHandler: navigating by action_url=$actionUrl, source=$source', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(actionUrl);
}
}

View File

@@ -10,17 +10,14 @@ import 'package:eatvio_app/features/registration/presentation/pages/verify_email
import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart'; import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
/// Маршруты для аутентификации
class AuthRoutes { class AuthRoutes {
/// Создание маршрутов аутентификации
static List<RouteBase> createRoutes() { static List<RouteBase> createRoutes() {
return [ return [
GoRoute( GoRoute(
path: RouteConstants.authScreen, path: RouteConstants.authScreen,
name: 'authScreen', name: 'authScreen',
builder: (context, state) => const AuthScreen(), builder: (context, state) => const AuthScreen(),
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
// и не должен блокировать навигацию
routes: [ routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()), GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),

View File

@@ -17,12 +17,20 @@ class ContentRoutes {
name: 'aboutPlanScreen', name: 'aboutPlanScreen',
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug']; final slug = state.uri.queryParameters['slug'];
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
if (slug != null) { if (slug != null) {
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); return AboutPlanScreen(slug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId);
} }
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); return AboutPlanScreen(
slug: data['slug'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
highlightedCommentId: data['commentId'] as int?,
);
}, },
), ),
GoRoute( GoRoute(
@@ -30,12 +38,20 @@ class ContentRoutes {
name: 'aboutRecipesScreen', name: 'aboutRecipesScreen',
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug']; final slug = state.uri.queryParameters['slug'];
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
if (slug != null) { if (slug != null) {
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); return AboutRecipeScreen(recipeSlug: slug, navigateToComments: commentId != null, index: null, highlightedCommentId: commentId);
} }
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); return AboutRecipeScreen(
recipeSlug: data['recipe'] as String,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
highlightedCommentId: data['commentId'] as int?,
);
}, },
), ),
GoRoute( GoRoute(
@@ -43,16 +59,28 @@ class ContentRoutes {
name: 'aboutDietScreen', name: 'aboutDietScreen',
builder: (context, state) { builder: (context, state) {
final slug = state.uri.queryParameters['slug']; final slug = state.uri.queryParameters['slug'];
final idStr = state.uri.queryParameters['id'];
final id = int.tryParse(idStr ?? '');
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
if (slug != null) { if (slug != null) {
return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false); return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId);
} }
final data = state.extra as Map<String, dynamic>; if (id != null && id > 0) {
return AboutDietScreen( return AboutDietScreen(id: id, slug: null, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId);
id: data['id'] as int?, }
slug: data['slug'] as String?, final data = state.extra as Map<String, dynamic>?;
index: data['index'] as int?, if (data != null) {
navigateToComments: data['navigateToComments'] as bool, return AboutDietScreen(
); id: data['id'] as int?,
slug: data['slug'] as String?,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool? ?? (data['commentId'] != null),
highlightedCommentId: data['commentId'] as int?,
);
}
return const AboutDietScreen(id: null, slug: null, index: null, navigateToComments: false, highlightedCommentId: null);
}, },
), ),
GoRoute( GoRoute(
@@ -65,12 +93,20 @@ class ContentRoutes {
name: 'blogPostScreen', name: 'blogPostScreen',
builder: (context, state) { builder: (context, state) {
final id = state.uri.queryParameters['id']; final id = state.uri.queryParameters['id'];
final commentIdStr = state.uri.queryParameters['commentId'];
final commentId = int.tryParse(commentIdStr ?? '');
final parsedId = int.tryParse(id ?? ''); final parsedId = int.tryParse(id ?? '');
if (parsedId != null) { if (parsedId != null) {
return BlogPostScreen(id: parsedId); return BlogPostScreen(id: parsedId, highlightedCommentId: commentId);
} }
return BlogPostScreen(id: state.extra as int); if (state.extra is int) {
return BlogPostScreen(id: state.extra as int);
}
final data = state.extra as Map<String, dynamic>;
return BlogPostScreen(id: data['id'] as int, highlightedCommentId: data['commentId'] as int?);
}, },
), ),
]; ];

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
@@ -31,8 +32,8 @@ class MainNavigationRoutes {
profileAvatarCubit.getMyProfile(); profileAvatarCubit.getMyProfile();
}); });
} }
return BottomNavigationBarWidget(child: child); return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
}, },
routes: [ routes: [
GoRoute( GoRoute(
@@ -45,7 +46,14 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.feedScreen, path: RouteConstants.feedScreen,
name: 'feedScreen', name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), pageBuilder: (context, state) {
final initialTabIndexFromQuery = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '');
final extra = state.extra;
if (extra is int) {
return NoTransitionPage(child: FeedScreen(initialTabIndex: extra));
}
return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndexFromQuery));
},
redirect: (context, state) async { redirect: (context, state) async {
try { try {
await sl<WsManager>().initialize(); await sl<WsManager>().initialize();
@@ -58,7 +66,7 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.diaryScreen, path: RouteConstants.diaryScreen,
name: 'diaryScreen', name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), pageBuilder: (context, state) => NoTransitionPage(child: DiaryScreen(date: state.extra as DateTime?)),
), ),
GoRoute( GoRoute(
path: ConsultationsScreen.path, path: ConsultationsScreen.path,
@@ -68,7 +76,10 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.chatSelectScreen, path: RouteConstants.chatSelectScreen,
name: 'chatSelectScreen', name: 'chatSelectScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), pageBuilder: (context, state) {
final initialSearchQuery = state.extra is String ? state.extra as String : null;
return NoTransitionPage(child: ChatsScreen(initialSearchQuery: initialSearchQuery));
},
), ),
GoRoute( GoRoute(
path: RouteConstants.notificationsScreen, path: RouteConstants.notificationsScreen,
@@ -78,19 +89,34 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.menuScreen, path: RouteConstants.menuScreen,
name: 'menuScreen', name: 'menuScreen',
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) => const NoTransitionPage(child: MenuScreen()),
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return MenuScreen(profileModel: state.profile);
}
return const SizedBox.shrink();
},
),
),
), ),
], ],
), ),
]; ];
} }
} }
/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications)
/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing.
class _MainShellWithNavScope extends StatefulWidget {
const _MainShellWithNavScope({required this.child, required this.currentLocation});
final Widget child;
final String currentLocation;
@override
State<_MainShellWithNavScope> createState() => _MainShellWithNavScopeState();
}
class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
final BottomNavController _navController = BottomNavController();
@override
Widget build(BuildContext context) {
return BottomNavScope(
controller: _navController,
child: BottomNavigationBarWidget(currentLocation: widget.currentLocation, child: widget.child),
);
}
}

View File

@@ -14,6 +14,7 @@ import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/video_calls/presentation/pages/video_call_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
@@ -156,10 +157,7 @@ class OtherRoutes {
builder: (context, state) { builder: (context, state) {
if (state.extra is Map<String, dynamic>) { if (state.extra is Map<String, dynamic>) {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
return RecipeCreateScreen( return RecipeCreateScreen(recipe: data['recipe'] as Recipe?, autoValidate: data['autoValidate'] as bool? ?? false);
recipe: data['recipe'] as Recipe?,
autoValidate: data['autoValidate'] as bool? ?? false,
);
} }
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null); return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
}, },
@@ -204,7 +202,7 @@ class OtherRoutes {
GoRoute( GoRoute(
path: '${RouteConstants.subsScreen}/:slug', path: '${RouteConstants.subsScreen}/:slug',
name: 'subsScreen', name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), builder: (context, state) => SubsScreen(slug: state.pathParameters['slug'], subType: state.extra as SubType),
), ),
// Chat routes // Chat routes
@@ -212,6 +210,10 @@ class OtherRoutes {
path: RouteConstants.chatScreen, path: RouteConstants.chatScreen,
name: 'chatScreen', name: 'chatScreen',
builder: (context, state) { builder: (context, state) {
final uuidFromQuery = state.uri.queryParameters['uuid'];
if (uuidFromQuery != null) {
return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false);
}
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String; final uuid = data['uuid'] as String;
final sendInitialMessage = data['sendInitialMessage'] as bool?; final sendInitialMessage = data['sendInitialMessage'] as bool?;
@@ -223,6 +225,27 @@ class OtherRoutes {
name: 'emptyChatScreen', name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(user: state.extra as ProfileModel), builder: (context, state) => EmptyChatScreen(user: state.extra as ProfileModel),
), ),
GoRoute(
path: RouteConstants.videoCallScreen,
name: VideoCallScreen.name,
builder: (context, state) {
final extra = state.extra;
if (extra is Map<String, dynamic>) {
return VideoCallScreen(
conversationUuid: extra['conversationUuid'] as String,
counterpartName: extra['counterpartName'] as String?,
localParticipantName: extra['localParticipantName'] as String?,
onBackToChat: extra['onBackToChat'] as void Function()?,
onHangUp: extra['onHangUp'] as void Function()?,
);
}
final uuid = state.uri.queryParameters['uuid'];
if (uuid != null) {
return VideoCallScreen(conversationUuid: uuid);
}
throw ArgumentError('VideoCallScreen: pass extra Map with conversationUuid or query uuid');
},
),
// Consultant routes // Consultant routes
GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
@@ -233,7 +256,7 @@ class OtherRoutes {
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final slug = data['slug'] as String?; final slug = data['slug'] as String?;
final isCanceled = data['isCanceled'] as bool?; final isCanceled = data['isCanceled'] as bool?;
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled,); return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled);
}, },
), ),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
@@ -267,6 +290,11 @@ class OtherRoutes {
path: InfoByOrderScreen.path, path: InfoByOrderScreen.path,
name: InfoByOrderScreen.name, name: InfoByOrderScreen.name,
builder: (context, state) { builder: (context, state) {
final queryParams = state.uri.queryParameters;
final planIdFromQuery = int.tryParse(queryParams['planId'] ?? '');
if (planIdFromQuery != null) {
return InfoByOrderScreen(planId: planIdFromQuery, successfulPayment: null, customerSlug: null, isConsultantView: true);
}
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int; final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?; final successfulPayment = data['successfulPayment'] as bool?;
@@ -290,8 +318,7 @@ class OtherRoutes {
builder: (context, state) { builder: (context, state) {
final data = state.extra as Map<String, int>; final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int; final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int; return UserDiaryScreen(diaryId: diaryId);
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
}, },
), ),
GoRoute( GoRoute(

View File

@@ -53,6 +53,7 @@ class RouteConstants {
static const String photoViewScreen = '/photo-view-screen'; static const String photoViewScreen = '/photo-view-screen';
static const String chatScreen = '/chat-screen'; static const String chatScreen = '/chat-screen';
static const String emptyChatScreen = '/empty-chat-screen'; static const String emptyChatScreen = '/empty-chat-screen';
static const String videoCallScreen = '/video-call-screen';
static const String glossaryScreen = '/glossary-screen'; static const String glossaryScreen = '/glossary-screen';
static const String blockedUsersScreen = '/blocked-users-screen'; static const String blockedUsersScreen = '/blocked-users-screen';
static const String notificationsSettingsScreen = '/notifications-settings-screen'; static const String notificationsSettingsScreen = '/notifications-settings-screen';

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
/// Helper class for managing themes and colors. /// Helper class for managing themes and colors.
class ThemeHelper { class ThemeHelper {
// The current app theme // The current app theme
@@ -200,6 +199,12 @@ class PrimaryColors {
Color get grey7B => const Color(0xFF7B7B7B); Color get grey7B => const Color(0xFF7B7B7B);
Color get greyD9 => const Color(0xFFD9D9D9); Color get greyD9 => const Color(0xFFD9D9D9);
Color get videoCallControlOffBackground => const Color(0xFFED2F2F);
Color get videoCallPipIconBackground => const Color(0xFF565656);
Color get videoCallNoticeBannerBackground => const Color(0xFF9D9D9D);
} }
PrimaryColors get appTheme => ThemeHelper().themeColor(); PrimaryColors get appTheme => ThemeHelper().themeColor();

View File

@@ -1,4 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:eatvio_app/core/views/cubit/views_state.dart'; import 'package:eatvio_app/core/views/cubit/views_state.dart';
import 'package:eatvio_app/core/views/repository/views_repository.dart'; import 'package:eatvio_app/core/views/repository/views_repository.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/models/view_model.dart';
@@ -86,9 +88,13 @@ class ViewsCubit extends Cubit<ViewsCubitState> {
} }
Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async { Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async {
await viewsRepository.viewContent( try {
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids), await viewsRepository.viewContent(
); viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
);
} catch (e, st) {
await reportFrom(e, st, this);
}
} }
void _scheduleDebouncedSend() { void _scheduleDebouncedSend() {

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/core/views/service/views_service.dart'; import 'package:eatvio_app/core/views/service/views_service.dart';
import 'package:eatvio_app/features/feed/models/view_model.dart'; import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ViewsRepository { class ViewsRepository {
final ViewsService viewsService; final ViewsService viewsService;
@@ -8,11 +7,7 @@ class ViewsRepository {
ViewsRepository({required this.viewsService}); ViewsRepository({required this.viewsService});
Future<void> viewContent({required ViewModel viewModel}) async { Future<void> viewContent({required ViewModel viewModel}) async {
try { await viewsService.viewContent(viewModel: viewModel);
await viewsService.viewContent(viewModel: viewModel);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -9,13 +9,16 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/websockets/ws_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
class WsManager { class WsManager {
final WsService wsService; final WsService wsService;
final ErrorReporter _errorReporter;
WsManager({required this.wsService}); WsManager({required this.wsService, required ErrorReporter errorReporter})
: _errorReporter = errorReporter;
PusherChannelsClient? client; PusherChannelsClient? client;
int? userId; int? userId;
@@ -111,13 +114,28 @@ class WsManager {
} }
}); });
_subscribe(); _subscribe();
await _connectCubitsToStreams();
} catch (e, st) { } catch (e, st) {
await Sentry.captureException(e, stackTrace: st); await _errorReporter.report(e, st, origin: 'WsManager');
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
} }
} }
/// Подключает ChatsCubit и NotificationsCubit к стримам сокета.
/// Вызывается после успешного initialize(), чтобы уведомления и чаты работали
/// и при входе уже залогиненного пользователя (редирект на feed), не только после экрана логина.
Future<void> _connectCubitsToStreams() async {
try {
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
} catch (e, st) {
log('WsManager._connectCubitsToStreams: $e', stackTrace: st);
await _errorReporter.report(e, st, origin: 'WsManager');
}
}
Future<void> _initPrivateNotificationsChannel() async { Future<void> _initPrivateNotificationsChannel() async {
final token = await TokenStorage.getTokenFromStorage(); final token = await TokenStorage.getTokenFromStorage();
_privateNotificationChannel = client?.privateChannel( _privateNotificationChannel = client?.privateChannel(

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart';
class AuthCubit extends Cubit<AuthState> { class AuthCubit extends Cubit<AuthState> {
@@ -26,7 +27,8 @@ class AuthCubit extends Cubit<AuthState> {
emit(AuthState.loaded(token: token)); emit(AuthState.loaded(token: token));
await TokenStorage.saveTokenInStorage(token); await TokenStorage.saveTokenInStorage(token);
return true; return true;
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString()); log(e.toString());
showErrorToast('Неверный адрес электронной почты или пароль'); showErrorToast('Неверный адрес электронной почты или пароль');
emit(AuthState.error(errorMessage: '$e')); emit(AuthState.error(errorMessage: '$e'));
@@ -37,22 +39,30 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> initializeUser() async { Future<void> initializeUser() async {
try { try {
await registerAnonymousPushToken(); await registerAnonymousPushToken();
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
} catch (e) { } catch (e, st) {
log('Error in initializeUser: $e'); log('Error in initializeUser: $e', stackTrace: st);
FlutterNativeSplash.remove(); FlutterNativeSplash.remove();
} }
} }
Future<void> registerAnonymousPushToken() async { Future<void> registerAnonymousPushToken() async {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; try {
if (!hasAnonymousTokenBeenSent) { final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
await firebaseApi.registerAnonymousPushToken(); if (!hasAnonymousTokenBeenSent) {
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true); await firebaseApi.registerAnonymousPushToken();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
}
} catch (e, st) {
await reportFrom(e, st, this);
} }
} }
Future<void> registerPushToken() async { Future<void> registerPushToken() async {
await firebaseApi.registerPushToken(); try {
await firebaseApi.registerPushToken();
} catch (e, st) {
await reportFrom(e, st, this);
}
} }
} }

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_request_respo
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin { class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final AuthRepository authRepository; final AuthRepository authRepository;
@@ -29,7 +30,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: Got token model: ${tokenModel.token}'); log('OAuthCubit: Got token model: ${tokenModel.token}');
TokenStorage.saveTokenInStorage(tokenModel.token); TokenStorage.saveTokenInStorage(tokenModel.token);
emit(OAuthState.loaded(token: tokenModel.token)); emit(OAuthState.loaded(token: tokenModel.token));
} on DioException catch (e) { } on DioException catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error occurred: $e'); log('OAuthCubit: Error occurred: $e');
// Проверяем, является ли ошибка ошибкой валидации email/phone // Проверяем, является ли ошибка ошибкой валидации email/phone
final errors = e.response?.data['errors'] as Map<String, dynamic>?; final errors = e.response?.data['errors'] as Map<String, dynamic>?;
@@ -62,7 +64,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
} else { } else {
emit(OAuthState.error(errorMessage: e.toString())); emit(OAuthState.error(errorMessage: e.toString()));
} }
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error occurred: $e'); log('OAuthCubit: Error occurred: $e');
emit(OAuthState.error(errorMessage: e.toString())); emit(OAuthState.error(errorMessage: e.toString()));
} }
@@ -103,7 +106,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
TokenStorage.saveTokenInStorage(token); TokenStorage.saveTokenInStorage(token);
cacheRegistrationDate(DateTime.now()); cacheRegistrationDate(DateTime.now());
emit(OAuthState.loaded(token: token)); emit(OAuthState.loaded(token: token));
} on DioException catch (e) { } on DioException catch (e, st) {
await reportFrom(e, st, this);
emit(OAuthState.error(errorMessage: e.toString())); emit(OAuthState.error(errorMessage: e.toString()));
final errors = e.response?.data['errors'] as Map<String, dynamic>?; final errors = e.response?.data['errors'] as Map<String, dynamic>?;
final emailErrors = errors?['email'] as List<dynamic>?; final emailErrors = errors?['email'] as List<dynamic>?;
@@ -153,7 +157,8 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
// Сбрасываем состояние после успешного запроса кода // Сбрасываем состояние после успешного запроса кода
emit(const OAuthState()); emit(const OAuthState());
return response; return response;
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error requesting verification code: $e'); log('OAuthCubit: Error requesting verification code: $e');
emit(OAuthState.error(errorMessage: e.toString())); emit(OAuthState.error(errorMessage: e.toString()));
return null; return null;
@@ -200,10 +205,10 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
log('OAuthCubit: OAuth verification confirmed successfully, token received'); log('OAuthCubit: OAuth verification confirmed successfully, token received');
TokenStorage.saveTokenInStorage(response.token); TokenStorage.saveTokenInStorage(response.token);
emit(OAuthState.loaded(token: response.token)); emit(OAuthState.loaded(token: response.token));
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log('OAuthCubit: Error confirming OAuth verification: $e'); log('OAuthCubit: Error confirming OAuth verification: $e');
emit(OAuthState.error(errorMessage: e.toString())); emit(OAuthState.error(errorMessage: e.toString()));
rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart';
import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ResetPasswordCubit extends Cubit<ResetPasswordState> { class ResetPasswordCubit extends Cubit<ResetPasswordState> {
final ResetPasswordRepository resetPasswordRepository; final ResetPasswordRepository resetPasswordRepository;
@@ -15,7 +16,8 @@ class ResetPasswordCubit extends Cubit<ResetPasswordState> {
} else { } else {
emit(const ResetPasswordState.codeSendError()); emit(const ResetPasswordState.codeSendError());
} }
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit(const ResetPasswordState.codeSendError()); emit(const ResetPasswordState.codeSendError());
} }
} }
@@ -28,7 +30,8 @@ class ResetPasswordCubit extends Cubit<ResetPasswordState> {
} else { } else {
emit(const ResetPasswordState.resetPasswordError()); emit(const ResetPasswordState.resetPasswordError());
} }
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit(const ResetPasswordState.resetPasswordError()); emit(const ResetPasswordState.resetPasswordError());
} }
} }

View File

@@ -21,7 +21,6 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -80,9 +79,6 @@ class _AuthScreenState extends State<AuthScreen> {
try { try {
await sl<ProfileAvatarCubit>().getMyProfile(); await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading(); await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken(); await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход'); showSuccessToast('Успешный вход');
} catch (e) { } catch (e) {
@@ -213,9 +209,6 @@ class _AuthScreenState extends State<AuthScreen> {
await sl<WsManager>().initialize(); await sl<WsManager>().initialize();
await sl<ProfileAvatarCubit>().getMyProfile(); await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading(); await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken(); await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешный вход'); showSuccessToast('Успешный вход');
} catch (e) { } catch (e) {

View File

@@ -13,7 +13,6 @@ import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/utils/validation_utils.dart'; import 'package:eatvio_app/core/utils/validation_utils.dart';
@@ -74,9 +73,6 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
try { try {
await sl<ProfileAvatarCubit>().getMyProfile(); await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading(); await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken(); await sl<FirebaseApi>().registerPushToken();
showSuccessToast('Успешная регистрация'); showSuccessToast('Успешная регистрация');
} catch (e) { } catch (e) {

View File

@@ -10,7 +10,6 @@ import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart'; import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
@@ -76,9 +75,6 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
try { try {
await sl<ProfileAvatarCubit>().getMyProfile(); await sl<ProfileAvatarCubit>().getMyProfile();
await sl<ChatsCubit>().getChatRoomsWithLoading(); await sl<ChatsCubit>().getChatRoomsWithLoading();
await sl<ChatsCubit>().connectToWebSocket();
await sl<NotificationsCubit>().getNotifications();
await sl<NotificationsCubit>().connectToWebSocket();
await sl<FirebaseApi>().registerPushToken(); await sl<FirebaseApi>().registerPushToken();
} catch (e) { } catch (e) {
rethrow; rethrow;

View File

@@ -7,7 +7,6 @@ import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_respo
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart';
import 'package:eatvio_app/features/auth/services/auth_service.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class AuthRepository { class AuthRepository {
final AuthService authService; final AuthService authService;
@@ -15,14 +14,10 @@ class AuthRepository {
AuthRepository({required this.authService}); AuthRepository({required this.authService});
Future<TokenModel> getTokenByCredentials(String email, String password) async { Future<TokenModel> getTokenByCredentials(String email, String password) async {
try { return await authService.getTokenByCredentials(
return await authService.getTokenByCredentials( credentials: CredentialsModel(email: email, password: password),
credentials: CredentialsModel(email: email, password: password), );
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<TokenModel> signInViaSocialMedia({ Future<TokenModel> signInViaSocialMedia({
@@ -31,14 +26,10 @@ class AuthRepository {
required String refreshToken, required String refreshToken,
required String clientId, required String clientId,
}) async { }) async {
try { return await authService.signInViaSocialMedia(
return await authService.signInViaSocialMedia( body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId), );
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<String> signUpViaSocialMedia({ Future<String> signUpViaSocialMedia({
@@ -56,47 +47,39 @@ class AuthRepository {
required int height, required int height,
required String clientId, required String clientId,
}) async { }) async {
try { return (await authService.signUpViaSocialMedia(
return (await authService.signUpViaSocialMedia( body: OAuthModel(
body: OAuthModel( provider: provider,
provider: provider, accessToken: accessToken,
accessToken: accessToken, id: id,
id: id, refreshToken: refreshToken,
refreshToken: refreshToken, firstName: firstName,
firstName: firstName, lastName: lastName,
lastName: lastName, email: email,
email: email, phone: phone,
phone: phone, dateOfBirth: dateOfBirth,
dateOfBirth: dateOfBirth, gender: gender == 'М' ? 'male' : 'female',
gender: gender == 'М' ? 'male' : 'female', weight: weight,
weight: weight, height: height,
height: height, clientId: clientId,
clientId: clientId, ),
), ))['token']!
))['token']! as String;
as String;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async { Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
try { Map<String, dynamic>? body;
Map<String, dynamic>? body; if (email != null) {
if (email != null) { body = {'email': email};
body = {'email': email}; } else if (phone != null) {
} else if (phone != null) { body = {'phone': phone};
body = {'phone': phone};
}
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
if (body != null) {
return await authService.requestOAuthVerification(body: body);
}
return null;
} }
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({ Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({
@@ -116,33 +99,28 @@ class AuthRepository {
required int weight, required int weight,
required int height, required int height,
}) async { }) async {
try { final request = OAuthVerificationConfirmRequest(
final request = OAuthVerificationConfirmRequest( provider: provider,
provider: provider, accessToken: accessToken,
accessToken: accessToken, refreshToken: refreshToken,
refreshToken: refreshToken, clientId: clientId,
clientId: clientId, id: id,
id: id, email: email,
email: email, phone: phone,
phone: phone, code: code,
code: code, verificationType: verificationType,
verificationType: verificationType, firstName: firstName,
firstName: firstName, lastName: lastName,
lastName: lastName, dateOfBirth: dateOfBirth,
dateOfBirth: dateOfBirth, gender: gender,
gender: gender, weight: weight,
weight: weight, height: height,
height: height, );
); // Логируем запрос перед отправкой
// Логируем запрос перед отправкой log(
log( 'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone', );
); return await authService.confirmOAuthVerification(body: request);
return await authService.confirmOAuthVerification(body: request);
} catch (e, s) {
log('AuthRepository: Error in confirmOAuthVerification: $e');
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -1,5 +1,4 @@
import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ResetPasswordRepository { class ResetPasswordRepository {
final ResetPasswordService resetPasswordService; final ResetPasswordService resetPasswordService;
@@ -9,22 +8,18 @@ class ResetPasswordRepository {
}); });
Future<bool> sendEmailCodeForPasswordChange(String email) async { Future<bool> sendEmailCodeForPasswordChange(String email) async {
try { final response = await resetPasswordService.sendEmailCodeForPasswordChange(
final response = await resetPasswordService.sendEmailCodeForPasswordChange( data: {
data: { 'email': email,
'email': email, },
}, );
); int statusCode = response.response.statusCode ?? 0;
int statusCode = response.response.statusCode ?? 0; if (statusCode >= 200 && statusCode < 300) {
if (statusCode >= 200 && statusCode < 300) { return true;
return true; } else {
} else { return false;
return false;
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
Future<bool> confirmPasswordChange( Future<bool> confirmPasswordChange(
@@ -33,24 +28,20 @@ class ResetPasswordRepository {
String passwordConfirmation, String passwordConfirmation,
String email, String email,
) async { ) async {
try { final response = await resetPasswordService.confirmPasswordChange(
final response = await resetPasswordService.confirmPasswordChange( data: {
data: { 'code': code,
'code': code, 'password': password,
'password': password, 'password_confirmation': passwordConfirmation,
'password_confirmation': passwordConfirmation, 'email': email,
'email': email, },
}, );
); int statusCode = response.response.statusCode ?? 0;
int statusCode = response.response.statusCode ?? 0; if (statusCode >= 200 && statusCode < 300) {
if (statusCode >= 200 && statusCode < 300) { return true;
return true; } else {
} else { return false;
return false;
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
} }

View File

@@ -8,10 +8,11 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:vk_id/vk_id.dart'; import 'package:vk_id/vk_id.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
@@ -43,7 +44,7 @@ class SocialAuthFacade {
} }
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e, s) { } catch (e, s) {
await Sentry.captureException(e, stackTrace: s); await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
return null; return null;
} }
} }
@@ -161,7 +162,7 @@ class SocialAuthFacade {
); );
} catch (e, s) { } catch (e, s) {
log(e.toString()); log(e.toString());
await Sentry.captureException(e, stackTrace: s); await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
} }
return null; return null;
} }
@@ -209,7 +210,7 @@ class SocialAuthFacade {
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
} catch (e, s) { } catch (e, s) {
await Sentry.captureException(e, stackTrace: s); await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
return null; return null;
} }
} }

View File

@@ -1,7 +1,6 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart'; import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart'; import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BarcodeRepository { class BarcodeRepository {
final BarcodeService barcodeService; final BarcodeService barcodeService;
@@ -15,49 +14,46 @@ class BarcodeRepository {
/// Получить информацию о продукте по штрихкоду /// Получить информацию о продукте по штрихкоду
/// Возвращает либо данные продукта, либо статус задачи обработки /// Возвращает либо данные продукта, либо статус задачи обработки
Future<BarcodeResult> getProductByBarcode(String barcode) async { Future<BarcodeResult> getProductByBarcode(String barcode) async {
try { final response = await dio.get<Map<String, dynamic>>(
final response = await dio.get('/api/gsone/barcode/$barcode'); '/api/gsone/barcode/$barcode',
options: Options(validateStatus: (_) => true),
if (response.statusCode == 200) { );
// Данные найдены в БД
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>); final code = response.statusCode ?? 0;
return BarcodeResult.success(product); final data = response.data;
} else if (response.statusCode == 202) { if (data is! Map<String, dynamic>) {
// Запущена задача обработки throw DioException(
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>); requestOptions: response.requestOptions,
return BarcodeResult.processing(taskStatus.taskId ?? ''); response: response,
} message: 'Invalid barcode response',
);
throw Exception('Unexpected status code: ${response.statusCode}');
} on DioException catch (e, s) {
if (e.response?.statusCode == 202) {
// Если получен статус 202, значит запущена задача обработки
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
await Sentry.captureException(e, stackTrace: s);
rethrow;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
if (code == 200) {
final product = BarcodeProductModel.fromJson(data);
return BarcodeResult.success(product);
}
if (code == 202) {
final taskStatus = BarcodeTaskStatusModel.fromJson(data);
return BarcodeResult.processing(taskStatus.taskId ?? '');
}
throw DioException(
requestOptions: response.requestOptions,
response: response,
message: 'Unexpected status code: $code',
);
} }
/// Получить статус задачи обработки /// Получить статус задачи обработки
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async { Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
try { final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
if (statusModel.status == 'completed' && statusModel.data != null) {
if (statusModel.status == 'completed' && statusModel.data != null) { return BarcodeTaskStatus.completed(statusModel.data!);
return BarcodeTaskStatus.completed(statusModel.data!); } else if (statusModel.status == 'failed') {
} else if (statusModel.status == 'failed') { return BarcodeTaskStatus.failed();
return BarcodeTaskStatus.failed(); } else {
} else { return BarcodeTaskStatus.processing();
return BarcodeTaskStatus.processing();
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
} }
@@ -109,4 +105,3 @@ enum BarcodeTaskStatusType {
processing, processing,
failed, failed,
} }

View File

@@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart'; import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class BlogPostCubit extends Cubit<BlogPostState> { class BlogPostCubit extends Cubit<BlogPostState> {
final FeedRepository feedRepository; final FeedRepository feedRepository;
@@ -16,6 +17,7 @@ class BlogPostCubit extends Cubit<BlogPostState> {
emit(state.copyWith(blogPostModel: postModel, isLoading: false)); emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false)); emit(state.copyWith(isLoading: false));
} }
@@ -37,6 +39,7 @@ class BlogPostCubit extends Cubit<BlogPostState> {
await feedRepository.likeBlogPost(id: current.id); await feedRepository.likeBlogPost(id: current.id);
} }
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
// rollback on error // rollback on error
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit(state.copyWith(blogPostModel: current)); emit(state.copyWith(blogPostModel: current));

View File

@@ -21,8 +21,9 @@ import 'package:share_plus/share_plus.dart';
class BlogPostScreen extends StatefulWidget { class BlogPostScreen extends StatefulWidget {
final int id; final int id;
final int? highlightedCommentId;
const BlogPostScreen({super.key, required this.id}); const BlogPostScreen({super.key, required this.id, this.highlightedCommentId});
@override @override
State<BlogPostScreen> createState() => _BlogPostScreenState(); State<BlogPostScreen> createState() => _BlogPostScreenState();
@@ -77,9 +78,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
bottomSheet: Container( bottomSheet: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
bottom: MediaQuery.of(context).padding.bottom + 8,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))), border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
@@ -146,14 +145,18 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
GestureDetector( GestureDetector(
onTap: () { onTap: () async {
if (textEditingController.text.length > 2) { if (textEditingController.text.length > 2) {
commentsCubit.sendComment( final isSent = await commentsCubit.sendComment(
id: widget.id.toString(), id: widget.id.toString(),
commentText: textEditingController.text, commentText: textEditingController.text,
parentCommentId: replyId, parentCommentId: replyId,
commentSourceType: CommentSourceType.blogPost, commentSourceType: CommentSourceType.blogPost,
); );
if (!context.mounted) return;
if (isSent) context.read<FeedCubit>().incrementCommentsState(widget.id);
context.read<CommentsCubit>().setReplyId(null); context.read<CommentsCubit>().setReplyId(null);
setState(() { setState(() {
textEditingController.clear(); textEditingController.clear();
@@ -236,9 +239,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false) if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: MarkdownHtmlWidget( child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description),
markdownText: blogPostState.blogPostModel!.description,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false) if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
@@ -303,7 +304,13 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
if (state is CommentsStateLoading) const Loader(), if (state is CommentsStateLoading) const Loader(),
if (state is CommentsStateLoaded) if (state is CommentsStateLoaded)
CommentsWidget(id: widget.id.toString(), commentSourceType: CommentSourceType.blogPost, comment: state.comment, replyId: replyId), CommentsWidget(
id: widget.id.toString(),
commentSourceType: CommentSourceType.blogPost,
comment: state.comment,
replyId: replyId,
highlightedCommentId: widget.highlightedCommentId,
),
], ],
), ),
), ),

View File

@@ -1,9 +1,12 @@
import 'package:eatvio_app/core/cubit/base_cubit.dart'; import 'dart:developer';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> { class BlogPostCreateCubit extends Cubit<BlogPostCreateState> {
final BlogPostRepository blogPostRepository; final BlogPostRepository blogPostRepository;
BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState()); BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState());
@@ -15,7 +18,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
emit(const BlogPostCreateState.loaded()); emit(const BlogPostCreateState.loaded());
return true; return true;
} catch (error, stackTrace) { } catch (error, stackTrace) {
handleError(error, stackTrace); log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
emit(BlogPostCreateState.error(errorMessage: error.toString())); emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false; return false;
} }
@@ -28,7 +32,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
emit(const BlogPostCreateState.loaded()); emit(const BlogPostCreateState.loaded());
return true; return true;
} catch (error, stackTrace) { } catch (error, stackTrace) {
handleError(error, stackTrace); log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
emit(BlogPostCreateState.error(errorMessage: error.toString())); emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false; return false;
} }
@@ -39,7 +44,8 @@ class BlogPostCreateCubit extends BaseCubit<BlogPostCreateState> {
await blogPostRepository.deleteBlogPost(id); await blogPostRepository.deleteBlogPost(id);
return true; return true;
} catch (error, stackTrace) { } catch (error, stackTrace) {
handleError(error, stackTrace); log('BlogPostCreateCubit: $error', stackTrace: stackTrace);
await reportFrom(error, stackTrace, this);
return false; return false;
} }
} }

View File

@@ -1,5 +1,5 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
@@ -8,7 +8,6 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class BlogPostRepository { class BlogPostRepository {
final BlogPostService blogPostService; final BlogPostService blogPostService;
@@ -23,48 +22,30 @@ class BlogPostRepository {
String? description, String? description,
XFile? image, XFile? image,
) async { ) async {
FormData formData = FormData.fromMap({ final formData = FormData.fromMap({
if (title != null && title.isNotEmpty) 'title': title, if (title != null && title.isNotEmpty) 'title': title,
if (description != null && description.isNotEmpty) 'description_md': description, if (description != null && description.isNotEmpty) 'description_md': description,
if (videoUrl != null && videoUrl.isNotEmpty) 'video_url': videoUrl, if (videoUrl != null && videoUrl.isNotEmpty) 'video_url': videoUrl,
if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name), if (image != null) 'image': await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name),
}); });
try { return blogPostService.createBlogPost(
return await blogPostService.createBlogPost( formData: formData,
formData: formData, );
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<XFile?> downloadImage(String imageUrl) async { Future<XFile?> downloadImage(String imageUrl) async {
try { final response = await http.get(Uri.parse('${sl<AppConfig>().baseUrl}/storage/$imageUrl'));
// Загрузка изображения
final response = await http.get(Uri.parse('${sl<AppConfig>().baseUrl}/storage/$imageUrl'));
if (response.statusCode == 200) { if (response.statusCode == 200) {
// Получение временной директории final directory = await getTemporaryDirectory();
final directory = await getTemporaryDirectory(); final filePath = '${directory.path}/image.jpg';
// Создание файла в временной директории final file = File(filePath);
final filePath = '${directory.path}/image.jpg'; // Вы можете изменить название файла await file.writeAsBytes(response.bodyBytes);
final file = File(filePath);
// Запись данных в файл
await file.writeAsBytes(response.bodyBytes);
// Возвращаем объект XFile return XFile(file.path);
return XFile(file.path);
} else {
log('Ошибка загрузки изображения: ${response.statusCode}');
return null;
}
} catch (e, s) {
log('Произошла ошибка: $e');
await Sentry.captureException(e, stackTrace: s);
return null;
} }
return null;
} }
Future<void> updateBlogPost( Future<void> updateBlogPost(
@@ -75,9 +56,9 @@ class BlogPostRepository {
XFile? image, XFile? image,
String? oldImageUrl, String? oldImageUrl,
) async { ) async {
FormData formData = FormData.fromMap({ final formData = FormData.fromMap({
'title': title, 'title': title,
'description_md': description, 'description_md': description,
'video_url': videoUrl, 'video_url': videoUrl,
'image': image != null 'image': image != null
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name) ? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name)
@@ -86,23 +67,13 @@ class BlogPostRepository {
: null, : null,
}); });
try { return blogPostService.updateBlogPost(
return await blogPostService.updateBlogPost( id: id,
id: id, formData: formData,
formData: formData, );
);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> deleteBlogPost(int id) async { Future<void> deleteBlogPost(int id) async {
try { await blogPostService.deleteBlogPost(id: id);
await blogPostService.deleteBlogPost(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -1,8 +1,11 @@
import 'dart:async';
import 'package:appmetrica_plugin/appmetrica_plugin.dart'; import 'package:appmetrica_plugin/appmetrica_plugin.dart';
import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@@ -28,7 +31,8 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
return age; return age;
} }
} catch (e) { } catch (e, st) {
unawaited(reportFrom(e, st, this));
return 0; return 0;
} }
return 0; return 0;
@@ -51,10 +55,12 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
]); ]);
AppMetrica.setUserProfileID(profile.slug); AppMetrica.setUserProfileID(profile.slug);
AppMetrica.reportUserProfile(userProfile); AppMetrica.reportUserProfile(userProfile);
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
// Игнорируем ошибки плагинов, профиль уже загружен // Игнорируем ошибки плагинов, профиль уже загружен
} }
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
AppRouter.navigatorKey.currentContext?.go('/auth-screen'); AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString())); emit(ProfileAvatarState.error(errorMessage: e.toString()));
} }

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
class BottomNavController {
BottomNavController();
void Function(String path, Object? extra)? _onSwitchToRoute;
void register(void Function(String path, Object? extra) callback) {
_onSwitchToRoute = callback;
}
void switchToRoute(String path, [Object? extra]) {
_onSwitchToRoute?.call(path, extra);
}
}
class BottomNavScope extends InheritedWidget {
const BottomNavScope({
required this.controller,
required super.child,
super.key,
});
final BottomNavController controller;
static BottomNavController? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<BottomNavScope>()?.controller;
}
static BottomNavController of(BuildContext context) {
final controller = maybeOf(context);
assert(controller != null, 'No BottomNavScope found in context');
return controller!;
}
@override
bool updateShouldNotify(BottomNavScope oldWidget) => controller != oldWidget.controller;
}

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/notifications/presentation/notifications_cub
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
@@ -18,9 +19,15 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class BottomNavigationBarWidget extends StatefulWidget { class BottomNavigationBarWidget extends StatefulWidget {
final Widget child; const BottomNavigationBarWidget({
super.key,
required this.child,
required this.currentLocation,
});
const BottomNavigationBarWidget({super.key, required this.child}); final Widget child;
/// Текущий путь из GoRouter (matchedLocation), чтобы подсветка таба совпадала с экраном при навигации через go() (диплинки, пуши).
final String currentLocation;
@override @override
State<BottomNavigationBarWidget> createState() => _BottomNavigationBarWidgetState(); State<BottomNavigationBarWidget> createState() => _BottomNavigationBarWidgetState();
@@ -66,6 +73,24 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
} }
} }
/// Switch to the tab that corresponds to [path], optionally with [extra]
/// (e.g. date for diary). If [path] is not in the current tab list, falls back to push.
void _switchToRoute(String path, [Object? extra]) {
if (pages == null) return;
final index = pages!.indexOf(path);
if (index >= 0 && index != currentBottomNavBarIndex) {
setState(() {
currentBottomNavBarIndex = index;
});
GoRouter.of(context).replace(path, extra: extra);
} else if (index >= 0) {
// Same tab but with new extra (e.g. diary with date)
GoRouter.of(context).replace(path, extra: extra);
} else {
GoRouter.of(context).push(path, extra: extra);
}
}
static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500 static const Color _devAccentColor = Color(0xFFF59E0B); // amber-500
Widget _buildLogoIcon() { Widget _buildLogoIcon() {
@@ -121,6 +146,22 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
// Инициализируем pages на основе profileModel // Инициализируем pages на основе profileModel
pages ??= _getPages(isConsultant); pages ??= _getPages(isConsultant);
// Expose tab switching to descendants (e.g. notification widget)
final controller = BottomNavScope.maybeOf(context);
if (controller != null) {
controller.register(_switchToRoute);
}
// Подсветка таба по текущему маршруту (совпадает при навигации через go(), в т.ч. диплинки/пуши).
final pathWithoutQuery = widget.currentLocation.split('?').first;
final indexFromRoute = pages!.indexOf(pathWithoutQuery);
final effectiveIndex = indexFromRoute >= 0 ? indexFromRoute : currentBottomNavBarIndex;
if (indexFromRoute >= 0 && indexFromRoute != currentBottomNavBarIndex) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() => currentBottomNavBarIndex = indexFromRoute);
});
}
return Scaffold( return Scaffold(
body: widget.child, body: widget.child,
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
@@ -129,7 +170,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
selectedFontSize: 10, selectedFontSize: 10,
unselectedFontSize: 10, unselectedFontSize: 10,
selectedItemColor: appTheme.blueA400, selectedItemColor: appTheme.blueA400,
currentIndex: currentBottomNavBarIndex, currentIndex: effectiveIndex,
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
label: '', label: '',
@@ -204,7 +245,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
ImageConstant.icnChat, ImageConstant.icnChat,
width: 20, width: 20,
height: 20, height: 20,
colorFilter: pages![currentBottomNavBarIndex] == '/chat-select-screen' colorFilter: pages![effectiveIndex] == '/chat-select-screen'
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), : const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
), ),
@@ -226,7 +267,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
child: SvgPicture.asset( child: SvgPicture.asset(
ImageConstant.icnBell, ImageConstant.icnBell,
colorFilter: pages![currentBottomNavBarIndex] == '/notifications-screen' colorFilter: pages![effectiveIndex] == '/notifications-screen'
? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)
: const ColorFilter.mode(Colors.grey, BlendMode.srcIn), : const ColorFilter.mode(Colors.grey, BlendMode.srcIn),
width: 20, width: 20,

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_state.dart'; import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_state.dart';
import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CalendarCubit extends Cubit<CalendarState> { class CalendarCubit extends Cubit<CalendarState> {
final CalendarRepository calendarRepository; final CalendarRepository calendarRepository;
@@ -12,7 +13,8 @@ class CalendarCubit extends Cubit<CalendarState> {
try { try {
final calendarList = await calendarRepository.getCalendar(); final calendarList = await calendarRepository.getCalendar();
emit(CalendarState.loaded(calendarList: calendarList)); emit(CalendarState.loaded(calendarList: calendarList));
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit(CalendarState.error(errorMessage: e.toString())); emit(CalendarState.error(errorMessage: e.toString()));
} }
} }

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/calendar/models/calendar_model.dart'; import 'package:eatvio_app/features/calendar/models/calendar_model.dart';
import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; import 'package:eatvio_app/features/calendar/services/calendar_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CalendarRepository { class CalendarRepository {
final CalendarService calendarService; final CalendarService calendarService;
@@ -8,11 +7,7 @@ class CalendarRepository {
CalendarRepository({required this.calendarService}); CalendarRepository({required this.calendarService});
Future<List<CalendarModel>> getCalendar() async { Future<List<CalendarModel>> getCalendar() async {
try { return await calendarService.getCalendar();
return await calendarService.getCalendar();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_s
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class CancelPlanCubit extends Cubit<CancelPlanState> { class CancelPlanCubit extends Cubit<CancelPlanState> {
final CancelPlanRepository cancelPlanRepository; final CancelPlanRepository cancelPlanRepository;
@@ -18,6 +19,7 @@ class CancelPlanCubit extends Cubit<CancelPlanState> {
await cancelPlanRepository.cancelChosenPlan(id: id, body: body); await cancelPlanRepository.cancelChosenPlan(id: id, body: body);
return true; return true;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
return false; return false;

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/models/cancelation_model.dart';
import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CancelPlanRepository { class CancelPlanRepository {
final CancelPlanService cancelService; final CancelPlanService cancelService;
@@ -9,11 +8,7 @@ class CancelPlanRepository {
required int id, required int id,
CancelationModel? body CancelationModel? body
}) async { }) async {
try { await cancelService.cancelChosenPlan(id: id, body: body);
await cancelService.cancelChosenPlan(id: id, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -20,6 +20,7 @@ import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatRoomCubit extends Cubit<ChatRoomState> { class ChatRoomCubit extends Cubit<ChatRoomState> {
@@ -64,7 +65,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
); );
currentPage++; currentPage++;
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
} }
} }
@@ -75,6 +77,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final cards = await cardsRepository.getCards(); final cards = await cardsRepository.getCards();
_setCard(cards: cards); _setCard(cards: cards);
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
} }
@@ -90,6 +93,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async { Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
try { try {
emit(const ChatRoomState.loading());
final chatRoom = await chatRepository.getChatRoom(uuid); final chatRoom = await chatRepository.getChatRoom(uuid);
ChosenPlanModel? chosenPlan; ChosenPlanModel? chosenPlan;
if (chatRoom.chosenPlanId != null) { if (chatRoom.chosenPlanId != null) {
@@ -111,7 +115,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (chosenPlan?.consultantId == chatRoom.users.sender.id) { if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
await checkCards(); await checkCards();
} }
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString()); log(e.toString());
typingSubscription?.cancel(); typingSubscription?.cancel();
messagesSubscription?.cancel(); messagesSubscription?.cancel();
@@ -121,13 +126,17 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
Future<void> updateChosenPlanState() async { Future<void> updateChosenPlanState() async {
final currentState = state; try {
if (currentState is ChatRoomStateLoaded) { final currentState = state;
final chosenPlanId = currentState.chatRoom.chosenPlanId; if (currentState is ChatRoomStateLoaded) {
if (chosenPlanId != null) { final chosenPlanId = currentState.chatRoom.chosenPlanId;
final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId); if (chosenPlanId != null) {
emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan)); final chosenPlan = await chatRepository.getChosenPlan(id: chosenPlanId);
emit(currentState.copyWith.chatRoom(chosenPlan: chosenPlan));
}
} }
} catch (e, st) {
await reportFrom(e, st, this);
} }
} }
@@ -142,6 +151,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: chosenPlan)); emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: chosenPlan));
return true; return true;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
return false; return false;
} }
@@ -151,7 +161,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try { try {
await chatRepository.cancelChosenPlan(id: planId); await chatRepository.cancelChosenPlan(id: planId);
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: null)); emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: null));
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
} }
} }
@@ -165,12 +176,12 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
AudioPlayer().play((AssetSource('audio/recive_message.wav'))); AudioPlayer().play((AssetSource('audio/recive_message.wav')));
final message = ChatMessageModel.fromJson(jsonDecode(event.data)); final message = ChatMessageModel.fromJson(jsonDecode(event.data));
final currentState = state as ChatRoomStateLoaded; final currentState = state as ChatRoomStateLoaded;
// Сообщения от текущего пользователя не приходят через WebSocket, // Сообщения от текущего пользователя не приходят через WebSocket,
// они обрабатываются через ответ API в sendMessage // они обрабатываются через ответ API в sendMessage
// Здесь обрабатываем только входящие сообщения от других пользователей // Здесь обрабатываем только входящие сообщения от других пользователей
List<ChatMessageModel> messagesList = [message, ...currentState.chatMessages]; List<ChatMessageModel> messagesList = [message, ...currentState.chatMessages];
chatRepository.emitNewMessageToChats(message); chatRepository.emitNewMessageToChats(message);
emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading)); emit(currentState.copyWith(chatMessages: messagesList, isLoading: isLoading));
EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms()); EasyDebounce.debounce('searchChatRoom', const Duration(milliseconds: 300), () => sl<ChatsCubit>().getChatRooms());
@@ -191,7 +202,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final messageUuid = uuid.v4(); final messageUuid = uuid.v4();
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null; final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
AudioPlayer().play((AssetSource('audio/send_message.wav'))); AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Создаем временное сообщение с состоянием loading // Создаем временное сообщение с состоянием loading
emit( emit(
(state as ChatRoomStateLoaded).copyWith( (state as ChatRoomStateLoaded).copyWith(
@@ -216,7 +227,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try { try {
// Отправляем сообщение и получаем ответ от сервера с правильными данными // Отправляем сообщение и получаем ответ от сервера с правильными данными
final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug); final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
// Заменяем локальное сообщение данными с сервера (с правильными URL файлов) // Заменяем локальное сообщение данными с сервера (с правильными URL файлов)
emit( emit(
(state as ChatRoomStateLoaded).copyWith( (state as ChatRoomStateLoaded).copyWith(
@@ -238,7 +249,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}).toList(), }).toList(),
), ),
); );
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit( emit(
(state as ChatRoomStateLoaded).copyWith( (state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
@@ -253,6 +265,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
sl<ChatsCubit>().getChatRooms(); sl<ChatsCubit>().getChatRooms();
return true; return true;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
return false; return false;
} }
@@ -274,6 +287,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
return false; return false;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
return false; return false;
} }
@@ -286,6 +300,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
final paymentModel = await chatRepository.getPaymentLink(id: planId); final paymentModel = await chatRepository.getPaymentLink(id: planId);
return paymentModel.paymentUrl; return paymentModel.paymentUrl;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
return null; return null;
} }
@@ -300,7 +315,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
body: PlanComplainModel(description: description), body: PlanComplainModel(description: description),
); );
return true; return true;
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString()); log(e.toString());
return false; return false;
} }
@@ -312,7 +328,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (planId == null) return false; if (planId == null) return false;
await chatRepository.finishChosenPlan(id: planId); await chatRepository.finishChosenPlan(id: planId);
return true; return true;
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString()); log(e.toString());
return false; return false;
} }
@@ -341,7 +358,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (message != null) { if (message != null) {
try { try {
AudioPlayer().play((AssetSource('audio/send_message.wav'))); AudioPlayer().play((AssetSource('audio/send_message.wav')));
// Определяем, есть ли файлы для повторной отправки // Определяем, есть ли файлы для повторной отправки
List<File>? filesToResend; List<File>? filesToResend;
if (message.mediaData != null && message.mediaData!.isNotEmpty) { if (message.mediaData != null && message.mediaData!.isNotEmpty) {
@@ -352,7 +369,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
filesToResend = message.mediaData!.cast<File>(); filesToResend = message.mediaData!.cast<File>();
} }
} }
emit( emit(
(state as ChatRoomStateLoaded).copyWith( (state as ChatRoomStateLoaded).copyWith(
chatMessages: [ chatMessages: [
@@ -375,12 +392,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
try { try {
// Отправляем сообщение и получаем ответ от сервера // Отправляем сообщение и получаем ответ от сервера
final serverMessage = await chatRepository.sendMessage( final serverMessage = await chatRepository.sendMessage(data: message.data, files: filesToResend, slug: userSlug);
data: message.data,
files: filesToResend,
slug: userSlug,
);
// Заменяем локальное сообщение данными с сервера // Заменяем локальное сообщение данными с сервера
emit( emit(
(state as ChatRoomStateLoaded).copyWith( (state as ChatRoomStateLoaded).copyWith(
@@ -400,7 +413,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}).toList(), }).toList(),
), ),
); );
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit( emit(
(state as ChatRoomStateLoaded).copyWith( (state as ChatRoomStateLoaded).copyWith(
chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) { chatMessages: (state as ChatRoomStateLoaded).chatMessages.map((elem) {
@@ -414,6 +428,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
} }
sl<ChatsCubit>().getChatRooms(); sl<ChatsCubit>().getChatRooms();
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
} }

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_sta
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ChatsCubit extends Cubit<ChatsState> { class ChatsCubit extends Cubit<ChatsState> {
final ChatRepository chatRepository; final ChatRepository chatRepository;
@@ -31,10 +32,14 @@ class ChatsCubit extends Cubit<ChatsState> {
late int lastPage; late int lastPage;
Future<void> connectToWebSocket() async { Future<void> connectToWebSocket() async {
await newIncomingMessagesSubscription?.cancel(); try {
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) { await newIncomingMessagesSubscription?.cancel();
_handleNewMessageViaSocket(event.data); newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
}); _handleNewMessageViaSocket(event.data);
});
} catch (e, st) {
await reportFrom(e, st, this);
}
} }
Future<void> getChatRoomsWithLoading({String? query}) async { Future<void> getChatRoomsWithLoading({String? query}) async {
@@ -53,7 +58,8 @@ class ChatsCubit extends Cubit<ChatsState> {
), ),
); );
page++; page++;
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
emit( emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error), state.copyWith(errorMessage: e.toString(), state: BaseState.error),
); );
@@ -76,6 +82,7 @@ class ChatsCubit extends Cubit<ChatsState> {
); );
page++; page++;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit( emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error), state.copyWith(errorMessage: e.toString(), state: BaseState.error),
@@ -99,6 +106,7 @@ class ChatsCubit extends Cubit<ChatsState> {
); );
page++; page++;
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit( emit(
state.copyWith( state.copyWith(
@@ -132,33 +140,37 @@ class ChatsCubit extends Cubit<ChatsState> {
} }
Future<void> _handleNewMessageViaSocket(dynamic data) async { Future<void> _handleNewMessageViaSocket(dynamic data) async {
final json = jsonDecode(data); try {
final message = ChatMessageModel.fromJson(json['message']); final json = jsonDecode(data);
final message = ChatMessageModel.fromJson(json['message']);
final currentChatRooms = [...state.chatRooms]; final currentChatRooms = [...state.chatRooms];
bool wasChanged = false; bool wasChanged = false;
bool willIncrementUnreadChats = false; bool willIncrementUnreadChats = false;
final updatedChatRooms = currentChatRooms.map((chatRoom) { final updatedChatRooms = currentChatRooms.map((chatRoom) {
if (chatRoom.id == message.roomId) { if (chatRoom.id == message.roomId) {
wasChanged = true; wasChanged = true;
if (chatRoom.unreadMessagesCount == 0) { if (chatRoom.unreadMessagesCount == 0) {
willIncrementUnreadChats = true; willIncrementUnreadChats = true;
}
return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1);
} }
return chatRoom.copyWith(lastMessage: message, unreadMessagesCount: chatRoom.unreadMessagesCount + 1); return chatRoom;
} }).toList();
return chatRoom;
}).toList();
if (!wasChanged && _query == null) { if (!wasChanged && _query == null) {
getChatRooms(); getChatRooms();
} else { } else {
emit( emit(
state.copyWith( state.copyWith(
chatRooms: updatedChatRooms, chatRooms: updatedChatRooms,
chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount, chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount,
), ),
); );
}
} catch (e, st) {
await reportFrom(e, st, this);
} }
} }

View File

@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
@@ -23,6 +24,7 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart';
import 'package:eatvio_app/features/video_calls/presentation/pages/video_call_screen.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@@ -62,6 +64,14 @@ class _ChatScreenState extends State<ChatScreen> {
chatRoomCubit.setTimer(); chatRoomCubit.setTimer();
} }
@override
void didUpdateWidget(ChatScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.uuid != widget.uuid) {
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -96,11 +106,34 @@ class _ChatScreenState extends State<ChatScreen> {
return _buildChat(context, state); return _buildChat(context, state);
} else if (state is ChatRoomStateError) { } else if (state is ChatRoomStateError) {
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)), body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
); );
} else { } else {
return const Scaffold(body: Loader()); return Scaffold(
body: const Loader(),
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
),
);
} }
}, },
), ),
@@ -148,8 +181,18 @@ class _ChatScreenState extends State<ChatScreen> {
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: AppBar(
titleSpacing: 0, titleSpacing: 0,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go(RouteConstants.chatSelectScreen);
},
),
backgroundColor: appTheme.gray100, backgroundColor: appTheme.gray100,
centerTitle: false, centerTitle: false,
title: GestureDetector( title: GestureDetector(
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug); GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
@@ -191,6 +234,23 @@ class _ChatScreenState extends State<ChatScreen> {
], ],
), ),
), ),
actions: [
Padding(
padding: const EdgeInsetsDirectional.only(end: 8),
child: _ChatHeaderVideoCallButton(
onPressed: () {
GoRouter.of(context).pushNamed(
VideoCallScreen.name,
extra: <String, dynamic>{
'conversationUuid': widget.uuid,
'counterpartName': state.chatRoom.users.receiver.name,
'localParticipantName': state.chatRoom.users.sender.name,
},
);
},
),
),
],
), ),
body: SafeArea( body: SafeArea(
child: Stack( child: Stack(
@@ -224,7 +284,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId; final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index]; final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order'; final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption; PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) { if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') { if (chosenPlan.optionType == 'options1') {
@@ -235,7 +295,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options; messageOption = chosenPlan.plan.packet3Options;
} }
} }
return Column( return Column(
children: [ children: [
_messageDate(message.updatedAt), _messageDate(message.updatedAt),
@@ -284,7 +344,7 @@ class _ChatScreenState extends State<ChatScreen> {
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId; final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
final message = state.chatMessages[index]; final message = state.chatMessages[index];
final isPlanOrderMessage = message.type == 'plan_order'; final isPlanOrderMessage = message.type == 'plan_order';
PacketOptions? messageOption; PacketOptions? messageOption;
if (isPlanOrderMessage && chosenPlan != null) { if (isPlanOrderMessage && chosenPlan != null) {
if (chosenPlan.optionType == 'options1') { if (chosenPlan.optionType == 'options1') {
@@ -295,7 +355,7 @@ class _ChatScreenState extends State<ChatScreen> {
messageOption = chosenPlan.plan.packet3Options; messageOption = chosenPlan.plan.packet3Options;
} }
} }
return Column( return Column(
children: [ children: [
if (isPlanOrderMessage && chosenPlan != null && messageOption != null) if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
@@ -699,3 +759,35 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
} }
class _ChatHeaderVideoCallButton extends StatelessWidget {
const _ChatHeaderVideoCallButton({required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final tooltipMessage = S.maybeOf(context)?.video_call;
final button = Material(
color: theme.colorScheme.onPrimary,
borderRadius: BorderRadius.circular(20),
elevation: 0,
child: InkWell(
onTap: onPressed,
customBorder: const CircleBorder(),
child: SizedBox(
width: 40,
height: 40,
child: Center(
child: Icon(Icons.videocam_rounded, color: appTheme.blueA400, size: 18),
),
),
),
);
if (tooltipMessage == null || tooltipMessage.isEmpty) {
return button;
}
return Tooltip(message: tooltipMessage, child: button);
}
}

View File

@@ -11,7 +11,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class ChatsScreen extends StatefulWidget { class ChatsScreen extends StatefulWidget {
const ChatsScreen({super.key}); const ChatsScreen({super.key, this.initialSearchQuery});
/// When set (e.g. from notification tap), the search field is pre-filled
/// and chat list is filtered by this query.
final String? initialSearchQuery;
@override @override
State<ChatsScreen> createState() => _ChatsScreenState(); State<ChatsScreen> createState() => _ChatsScreenState();
@@ -23,12 +27,17 @@ class _ChatsScreenState extends State<ChatsScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Инициализация ChatsCubit при первом открытии экрана final query = widget.initialSearchQuery?.trim();
if (query != null && query.isNotEmpty) {
searchController.text = query;
}
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final cubit = context.read<ChatsCubit>(); final cubit = context.read<ChatsCubit>();
final state = cubit.state; final state = cubit.state;
if (state.state == BaseState.initial) { if (state.state == BaseState.initial) {
cubit.getChatRoomsWithLoading(); cubit.getChatRoomsWithLoading(query: query?.isNotEmpty == true ? query : null);
} else if (query != null && query.isNotEmpty) {
cubit.getChatRooms(query: query);
} }
}); });
} }

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
@@ -12,7 +11,6 @@ import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ChatRepository { class ChatRepository {
final ChatService chatService; final ChatService chatService;
@@ -34,160 +32,104 @@ class ChatRepository {
String? data, String? data,
List<File>? files, List<File>? files,
}) async { }) async {
try { final multipartFiles = <MultipartFile>[];
final multipartFiles = <MultipartFile>[];
if (files != null) { if (files != null) {
for (final file in files) { for (final file in files) {
final fileBytes = await file.readAsBytes(); final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes( final multipartFile = MultipartFile.fromBytes(
fileBytes, fileBytes,
filename: file.path.split('/').last, filename: file.path.split('/').last,
contentType: MediaType('application', 'octet-stream'), contentType: MediaType('application', 'octet-stream'),
); );
multipartFiles.add(multipartFile); multipartFiles.add(multipartFile);
}
} }
// Если data пустая строка или null, не передаем её вообще
// Сервер требует либо непустую строку, либо отсутствие поля
final dataToSend = (data == null || data.trim().isEmpty) ? null : data;
return await chatService.sendMessage(
slug: slug,
data: dataToSend,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
} }
// Если data пустая строка или null, не передаем её вообще
// Сервер требует либо непустую строку, либо отсутствие поля
final dataToSend = (data == null || data.trim().isEmpty) ? null : data;
return await chatService.sendMessage(
slug: slug,
data: dataToSend,
mediaData: multipartFiles.isEmpty ? null : multipartFiles,
socketId: (await SocketIdStorage.getSocketIdFromStorage())!,
);
} }
Future<ChatRoomModel> getChatRoom(String uuid) async { Future<ChatRoomModel> getChatRoom(String uuid) async {
try { return (await chatService.getChatRoom(uuid: uuid));
return (await chatService.getChatRoom(uuid: uuid));
} catch (e, st) {
log(e.toString(), stackTrace: st);
await Sentry.captureException(e, stackTrace: st);
rethrow;
}
} }
Future<ChatRoomPaginationModel> getChatRooms({String? query, int page = 1}) async { Future<ChatRoomPaginationModel> getChatRooms({String? query, int page = 1}) async {
try { return (await chatService.getChatRooms(query: query, page: page));
return (await chatService.getChatRooms(query: query, page: page));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<ChatMessagesModel> getChatMessages(String id, int page) async { Future<ChatMessagesModel> getChatMessages(String id, int page) async {
try { return (await chatService.getChatMessages(id: id, page: page));
return (await chatService.getChatMessages(id: id, page: page));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<ChatRoomModel> getChatRoomIdByUserSlug({required String slug}) async { Future<ChatRoomModel> getChatRoomIdByUserSlug({required String slug}) async {
try { return (await chatService.getChatRoomIdByUserSlug(user: slug));
return (await chatService.getChatRoomIdByUserSlug(user: slug));
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> markMessagesAsRead(String id, List<int> messageIds) async { Future<void> markMessagesAsRead(String id, List<int> messageIds) async {
try { await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
await chatService.markMessagesAsRead(id: id, body: {'messages_ids': messageIds});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> cancelChosenPlan({ Future<void> cancelChosenPlan({
required int id, required int id,
}) async { }) async {
try { await chatService.cancelChosenPlan(id: id);
await chatService.cancelChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> choseStartPlanDate({required int id, required Map<String, String> date}) async { Future<void> choseStartPlanDate({required int id, required Map<String, String> date}) async {
try { await chatService.choseStartPlanDate(id: id, date: date);
await chatService.choseStartPlanDate(id: id, date: date);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<PaymentModel> getPaymentLink({ Future<PaymentModel> getPaymentLink({
required int id, required int id,
}) async { }) async {
try { return await chatService.getPaymentLink(id: id);
return await chatService.getPaymentLink(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<ChosenPlanModel> getChosenPlan({ Future<ChosenPlanModel> getChosenPlan({
required int id, required int id,
}) async { }) async {
try { return await chatService.getChosenPlan(id: id);
return await chatService.getChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> finishChosenPlan({ Future<void> finishChosenPlan({
required int id, required int id,
}) async { }) async {
try { await chatService.finishChosenPlan(id: id);
await chatService.finishChosenPlan(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> complainAboutConsultation({ Future<void> complainAboutConsultation({
required int id, required int id,
required PlanComplainModel body, required PlanComplainModel body,
}) async { }) async {
try { await chatService.complainAboutConsultation(id: id, body: body);
await chatService.complainAboutConsultation(id: id, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<String> getChatByUserSlug(String slug) async { Future<String> getChatByUserSlug(String slug) async {
try { final response = await chatService.getChatByUserSlug(slug: slug);
final response = await chatService.getChatByUserSlug(slug: slug); if (response.response.statusCode == 404) {
if (response.response.statusCode == 404) { return '404';
return '404';
} else {
return response.data['id'];
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
return 'error';
} }
return response.data['id'];
} }
} }

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart'; import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_state.dart';
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ComplainingCubit extends Cubit<ComplainingState> { class ComplainingCubit extends Cubit<ComplainingState> {
final ComplainingRepository repository; final ComplainingRepository repository;
@@ -81,6 +82,7 @@ class ComplainingCubit extends Cubit<ComplainingState> {
} }
} }
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
emit(state.copyWith(wasSentSuccessfully: false)); emit(state.copyWith(wasSentSuccessfully: false));

View File

@@ -1,6 +1,5 @@
import 'package:eatvio_app/features/complaining/models/complain_model.dart'; import 'package:eatvio_app/features/complaining/models/complain_model.dart';
import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ComplainingRepository { class ComplainingRepository {
final ComplainingService service; final ComplainingService service;
@@ -11,12 +10,8 @@ class ComplainingRepository {
required String category, required String category,
required ComplainModel body, required ComplainModel body,
}) async { }) async {
try { await service.sendPlanComplain(slug: slug, category: category, body: body);
await service.sendPlanComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> sendRecipeComplain({ Future<void> sendRecipeComplain({
@@ -24,12 +19,8 @@ class ComplainingRepository {
required String category, required String category,
required ComplainModel body, required ComplainModel body,
}) async { }) async {
try { await service.sendRecipeComplain(slug: slug, category: category, body: body);
await service.sendRecipeComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> sendBlogPostsComplain({ Future<void> sendBlogPostsComplain({
@@ -37,12 +28,8 @@ class ComplainingRepository {
required String category, required String category,
required ComplainModel body, required ComplainModel body,
}) async { }) async {
try { await service.sendBlogPostsComplain(id: id, category: category, body: body);
await service.sendBlogPostsComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> sendProfileComplain({ Future<void> sendProfileComplain({
@@ -50,12 +37,8 @@ class ComplainingRepository {
required String category, required String category,
required ComplainModel body, required ComplainModel body,
}) async { }) async {
try { await service.sendProfileComplain(slug: slug, category: category, body: body);
await service.sendProfileComplain(slug: slug, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> sendDietComplain({ Future<void> sendDietComplain({
@@ -63,12 +46,8 @@ class ComplainingRepository {
required String category, required String category,
required ComplainModel body, required ComplainModel body,
}) async { }) async {
try { await service.sendDietComplain(id: id, category: category, body: body);
await service.sendDietComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> sendCommentComplain({ Future<void> sendCommentComplain({
@@ -76,11 +55,7 @@ class ComplainingRepository {
required String category, required String category,
required ComplainModel body, required ComplainModel body,
}) async { }) async {
try { await service.sendCommentComplain(id: id, category: category, body: body);
await service.sendCommentComplain(id: id, category: category, body: body);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -1,9 +1,11 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class RegisterConsultantCubit extends Cubit<RegisterConsultantState> { class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
final ConsultantRepository consultantRepository; final ConsultantRepository consultantRepository;
@@ -20,70 +22,106 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
); );
Future<void> getMyRequestStatus(String slug) async { Future<void> getMyRequestStatus(String slug) async {
_update = true; try {
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug); _update = true;
final certPhotos = await consultantRepository.getConsultantCertPhotos(); final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
final certPhotos = await consultantRepository.getConsultantCertPhotos();
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) { List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first; final course = courseMap.values.first;
return CertificatesModel(title: course.title, date: course.date); return CertificatesModel(title: course.title, date: course.date);
}).toList(); }).toList();
String? sanitizeLink(String? link) { String? sanitizeLink(String? link) {
return (link != null && link.isNotEmpty) ? link : null; return (link != null && link.isNotEmpty) ? link : null;
} }
emit( emit(
RegisterConsultantState( RegisterConsultantState(
consultant: ConsultantProfileModel( consultant: ConsultantProfileModel(
certificates: certificates, certificates: certificates,
socialLinks: SocialLinksModel( socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink), vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink), tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink), fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink), instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink), personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
), ),
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1, acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
certPhotos: certPhotos,
isUpdating: _update,
error: null,
), ),
acceptOffer: false, );
loading: false, } catch (e, st) {
requestModel: myRequestStatus, await reportFrom(e, st, this);
certPhotos: certPhotos, log(e.toString());
isUpdating: _update emit(state.copyWith(loading: false, error: e.toString()));
), }
);
} }
Future<void> disableLoading() async { Future<void> disableLoading() async {
final certPhotos = await consultantRepository.getConsultantCertPhotos(); try {
emit(state.copyWith(loading: false, certPhotos: certPhotos)); final certPhotos = await consultantRepository.getConsultantCertPhotos();
emit(state.copyWith(loading: false, certPhotos: certPhotos, error: null));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(loading: false, error: e.toString()));
}
}
/// Повторная загрузка после [DataErrorWidget], аналогично [AboutDietCubit.getDiet].
Future<void> reloadData(String? slug) async {
emit(state.copyWith(loading: true, error: null));
if (slug != null) {
await getMyRequestStatus(slug);
} else {
await disableLoading();
}
} }
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) { void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false)); emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false, error: null));
} }
Future<void> registerConsultant() async { Future<void> registerConsultant() async {
if (state.newAddedPhotos.isNotEmpty) { emit(state.copyWith(error: null));
await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos); try {
} if (state.newAddedPhotos.isNotEmpty) {
if (state.deletedPhotoIds.isNotEmpty) { await consultantRepository.storeConsultantCertPhotos(files: state.newAddedPhotos);
for (final int id in state.deletedPhotoIds) {
await consultantRepository.deletePhoto(id);
} }
if (state.deletedPhotoIds.isNotEmpty) {
for (final int id in state.deletedPhotoIds) {
await consultantRepository.deletePhoto(id);
}
}
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(error: e.toString()));
} }
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
} }
Future<void> addCertPhotos(List<File> files) async { Future<void> addCertPhotos(List<File> files) async {
if (files.isEmpty) { if (files.isEmpty) {
return; return;
} }
final compressedFiles = await ImageCompressUtility.compressFilesList(files); try {
final compressedFiles = await ImageCompressUtility.compressFilesList(files);
emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles])); emit(state.copyWith(newAddedPhotos: [...state.newAddedPhotos, ...compressedFiles], error: null));
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
emit(state.copyWith(error: e.toString()));
}
} }
Future<void> removeCertPhoto(int? id, String? path) async { Future<void> removeCertPhoto(int? id, String? path) async {

View File

@@ -18,5 +18,6 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
@Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos, @Default(<ConsultantCertPhotoModel>[]) List<ConsultantCertPhotoModel> certPhotos,
@Default(<File>[]) List<File> newAddedPhotos, @Default(<File>[]) List<File> newAddedPhotos,
@Default(<int>[]) List<int> deletedPhotoIds, @Default(<int>[]) List<int> deletedPhotoIds,
String? error,
}) = _RegisterConsultantState; }) = _RegisterConsultantState;
} }

View File

@@ -17,18 +17,13 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
title: const Text('Стать консультантом'), title: const Text('Стать консультантом'),
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
centerTitle: false, centerTitle: false,
leading: IconButton( leading: IconButton(onPressed: () => GoRouter.of(context).pop(), icon: const Icon(Icons.close)),
onPressed: () => GoRouter.of(context).pop(),
icon: const Icon(Icons.close),
),
), ),
body: const Padding( body: const Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Text( child: Text(
'Чтобы стать консультантом необходимо пройти короткую регистрацию. Подготовьте свои дипломы и/или сертификаты в формате jpg, они Вам пригодятся для проверки и подтверждения. Также необходимо будет указать ссылки на Ваши соц. сети и указать тип лица (физ. лицо или юр. лицо). На данный момент доступна регистрация в качестве физ. лица.\n\nВнутри Итвио разница между обычным пользователем и консультантом заключается в дополнительных возможностях последнего создавать публичные диеты, рецепты и планы питания; участвовать в ежемесячных рейтингах.', 'Чтобы стать консультантом необходимо пройти короткую регистрацию. Подготовьте свои дипломы и/или сертификаты в формате jpg, они Вам пригодятся для проверки и подтверждения. Также необходимо будет указать ссылки на Ваши соц. сети и указать тип лица (физ. лицо или юр. лицо). На данный момент доступна регистрация в качестве физ. лица.\n\nВнутри Итвио разница между обычным пользователем и консультантом заключается в дополнительных возможностях последнего создавать публичные диеты, рецепты и планы питания; участвовать в ежемесячных рейтингах.',
style: TextStyle( style: TextStyle(fontSize: 16),
fontSize: 16,
),
), ),
), ),
bottomSheet: _startRegistrationButton(), bottomSheet: _startRegistrationButton(),
@@ -37,24 +32,16 @@ class _BecomeConsultantScreenState extends State<BecomeConsultantScreen> {
InkWell _startRegistrationButton() { InkWell _startRegistrationButton() {
return InkWell( return InkWell(
onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen'), onTap: () => GoRouter.of(context).pushReplacementNamed('stepperBecomeConsultantScreen', extra: {'slug': null}),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: Ink( child: Ink(
height: 52, height: 52,
width: MediaQuery.of(context).size.width - 32, width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration( decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center( child: const Center(
child: Text( child: Text(
'Начать регистрацию', 'Начать регистрацию',
style: TextStyle( style: TextStyle(color: Colors.white, height: 1, fontWeight: FontWeight.w600, fontSize: 18),
color: Colors.white,
height: 1,
fontWeight: FontWeight.w600,
fontSize: 18,
),
), ),
), ),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
@@ -9,6 +10,7 @@ import 'package:eatvio_app/features/consultant/become_consultant/presentation/pa
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/social_networks_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/social_networks_screen.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -48,8 +50,37 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
create: (context) => registrationConsultantCubit, create: (context) => registrationConsultantCubit,
child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>( child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
builder: (context, state) { builder: (context, state) {
if (state.loading == false) { if (state.loading) {
return const Scaffold(body: Loader());
}
if (state.error != null) {
return Scaffold( return Scaffold(
appBar: AppBar(
centerTitle: true,
leading: BackButton(
onPressed: () {
if (context.canPop()) {
GoRouter.of(context).pop();
return;
}
GoRouter.of(context).go('/feed-screen');
},
),
title: Text(
'',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: DataErrorWidget(
onReload: () => context.read<RegisterConsultantCubit>().reloadData(widget.slug),
),
);
}
return Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: Row( floatingActionButton: Row(
children: [ children: [
@@ -136,6 +167,9 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
try { try {
await registrationConsultantCubit.registerConsultant(); await registrationConsultantCubit.registerConsultant();
if (!context.mounted) return; if (!context.mounted) return;
if (registrationConsultantCubit.state.error != null) {
return;
}
await showModalBottomSheet( await showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
showDragHandle: true, showDragHandle: true,
@@ -183,7 +217,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
); );
} catch (e) { } catch (e) {
if (!context.mounted) return; if (!context.mounted) return;
showDialog( showDialog<void>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
actionsPadding: const EdgeInsets.symmetric(horizontal: 4), actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
@@ -191,9 +225,7 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
actions: [ actions: [
TextButton( TextButton(
child: const Text('Ок'), child: const Text('Ок'),
onPressed: () { onPressed: () => Navigator.of(context).pop(),
Navigator.of(context).pop();
},
), ),
], ],
), ),
@@ -282,9 +314,6 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
], ],
), ),
); );
} else {
return const Scaffold(body: Loader());
}
}, },
), ),
); );

View File

@@ -5,7 +5,6 @@ import 'package:eatvio_app/features/consultant/become_consultant/models/consulta
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart'; import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ConsultantRepository { class ConsultantRepository {
final ConsultantService consultantService; final ConsultantService consultantService;
@@ -13,90 +12,58 @@ class ConsultantRepository {
const ConsultantRepository({required this.consultantService}); const ConsultantRepository({required this.consultantService});
Future<void> registerConsultant({required ConsultantProfileModel consultant, bool update = false}) async { Future<void> registerConsultant({required ConsultantProfileModel consultant, bool update = false}) async {
try { if (update) {
if (update) { return await consultantService.consultantUpdate(consultantModel: consultant);
return await consultantService.consultantUpdate(consultantModel: consultant); } else {
} else { return await consultantService.consultantRegistration(consultantModel: consultant);
return await consultantService.consultantRegistration(consultantModel: consultant);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
Future<ConsultationRequestModel> getConsultantRequestStatus(String slug) async { Future<ConsultationRequestModel> getConsultantRequestStatus(String slug) async {
try { return await consultantService.getConsultantRequestStatus(slug: slug);
return await consultantService.getConsultantRequestStatus(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<ConsultationRequestModel> getMyRequestStatus(String slug) async { Future<ConsultationRequestModel> getMyRequestStatus(String slug) async {
try { return await consultantService.getMyRequestStatus(slug: slug);
return await consultantService.getMyRequestStatus(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List<ConsultantCertPhotoModel>> getConsultantCertPhotos() async { Future<List<ConsultantCertPhotoModel>> getConsultantCertPhotos() async {
try { return await consultantService.getConsultantCertPhotos();
return await consultantService.getConsultantCertPhotos();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({required List<File> files}) async { Future<List<ConsultantCertPhotoModel>> storeConsultantCertPhotos({required List<File> files}) async {
try { final multipartFiles = <MultipartFile>[];
final multipartFiles = <MultipartFile>[];
for (final file in files) { for (final file in files) {
final fileBytes = await file.readAsBytes(); final fileBytes = await file.readAsBytes();
final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream')); final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream'));
multipartFiles.add(multipartFile); multipartFiles.add(multipartFile);
}
if (multipartFiles.isEmpty) {
return [];
}
return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
if (multipartFiles.isEmpty) {
return [];
}
return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
} }
Future<void> hideConsultantCertPhoto(int id) async { Future<void> hideConsultantCertPhoto(int id) async {
try { await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> unHideConsultantCertPhoto(int id) async { Future<void> unHideConsultantCertPhoto(int id) async {
try { await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> deletePhoto(int id) async { Future<void> deletePhoto(int id) async {
try { await consultantService.deletePhoto(id: id);
await consultantService.deletePhoto(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/consultant_rating/repositories/consultant_ra
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> { class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final ConsultantRatingRepository repository; final ConsultantRatingRepository repository;
@@ -20,6 +21,7 @@ class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name); final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name);
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores)); emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores));
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isStatisticsLoading: true, error: e.toString())); emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
@@ -31,6 +33,7 @@ class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
final scoreLogs = await repository.getScoreLogs(date: date); final scoreLogs = await repository.getScoreLogs(date: date);
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null)); emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isRatingLoading: true, error: e.toString())); emit(state.copyWith(isRatingLoading: true, error: e.toString()));
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }

View File

@@ -1,7 +1,6 @@
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart'; import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart'; import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class ConsultantRatingRepository { class ConsultantRatingRepository {
final ConsultantRatingService service; final ConsultantRatingService service;
@@ -9,21 +8,13 @@ class ConsultantRatingRepository {
ConsultantRatingRepository(this.service); ConsultantRatingRepository(this.service);
Future<StatisticsModel> getConsultantStatistics({String? date}) async { Future<StatisticsModel> getConsultantStatistics({String? date}) async {
try { return await service.getConsultantStatistics(date: date);
return await service.getConsultantStatistics(date: date);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List<RatingModel>> getScoreLogs({String? date}) async { Future<List<RatingModel>> getScoreLogs({String? date}) async {
try { final data = (await service.getScoreLogs(date: date)).data;
final data = (await service.getScoreLogs(date: date)).data; return data;
return data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart'; import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultationsCubit extends Cubit<ConsultationsState> { class ConsultationsCubit extends Cubit<ConsultationsState> {
final ConsultationsRepository consultationsRepository; final ConsultationsRepository consultationsRepository;
@@ -14,6 +15,7 @@ class ConsultationsCubit extends Cubit<ConsultationsState> {
final consultations = await consultationsRepository.getConsultations(); final consultations = await consultationsRepository.getConsultations();
emit(state.copyWith(consultations: consultations, state: BaseState.success)); emit(state.copyWith(consultations: consultations, state: BaseState.success));
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit(state.copyWith(state: BaseState.error)); emit(state.copyWith(state: BaseState.error));
} }

View File

@@ -172,8 +172,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
); );
return; return;
} }
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id, 'currentDiaryDay': index + 1}); GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': day.$2.id,});
}, },
borderRadius: BorderRadius.circular(100), borderRadius: BorderRadius.circular(100),
child: Ink( child: Ink(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -255,7 +255,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
.where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished) .where((e) => e.$2.state == DiaryStateEnum.finished || e.$2.state == DiaryStateEnum.lastDayFinished)
.lastOrNull; .lastOrNull;
if (lastActiveDay != null) { if (lastActiveDay != null) {
GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id, 'currentDiaryDay': lastActiveDay.$1}); GoRouter.of(context).push(UserDiaryScreen.path, extra: {'diaryId': lastActiveDay.$2.id,});
return; return;
} }
showErrorDialog( showErrorDialog(

View File

@@ -1,7 +1,6 @@
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart'; import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart'; import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart'; import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
@@ -11,13 +10,8 @@ class ConsultationsRepository {
ConsultationsRepository({required this.consultationService}); ConsultationsRepository({required this.consultationService});
Future<List<ConsultationModel>> getConsultations() async { Future<List<ConsultationModel>> getConsultations() async {
try { final consultations = await consultationService.getConsultations();
final consultations = await consultationService.getConsultations(); return consultations.data;
return consultations.data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List<ConsultationModel>> getFilteredConsultations({ Future<List<ConsultationModel>> getFilteredConsultations({
@@ -26,44 +20,24 @@ class ConsultationsRepository {
FiltersComparisonEnum? stateComparison, FiltersComparisonEnum? stateComparison,
FiltersComparisonEnum? dateComparison, FiltersComparisonEnum? dateComparison,
}) async { }) async {
try { final model = await consultationService.getFilteredConsultations(
final model = await consultationService.getFilteredConsultations( state: state,
state: state, date: date,
date: date, stateComparison: stateComparison,
stateComparison: stateComparison, dateComparison: dateComparison,
dateComparison: dateComparison, );
); return model.data;
return model.data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<StatisticModel> getStatistic() async { Future<StatisticModel> getStatistic() async {
try { return consultationService.getStatistics();
return await consultationService.getStatistics();
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List<ConsultantFinanceModel>> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async { Future<List<ConsultantFinanceModel>> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async {
try { return consultationService.getConsultantFinances(date, dateComparison);
return await consultationService.getConsultantFinances(date, dateComparison);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List<UserPurchasesModel>> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async { Future<List<UserPurchasesModel>> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async {
try { return consultationService.getUserPurchases(date, dateComparison);
return await consultationService.getUserPurchases(date, dateComparison);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -103,7 +103,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
if (state.published) { if (state.published) {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return; if (!context.mounted) return;
context.pop(); context.pop(true);
showBottomSheetWithBlur( showBottomSheetWithBlur(
userRootNavigator: true, userRootNavigator: true,
context: context, context: context,
@@ -113,7 +113,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else if (state.drafted) { } else if (state.drafted) {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return; if (!context.mounted) return;
context.pop(); context.pop(true);
showBottomSheetWithBlur( showBottomSheetWithBlur(
context: context, context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true), child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
@@ -231,7 +231,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
if (dietState == DietState.draft) { if (dietState == DietState.draft) {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return; if (!context.mounted) return;
context.pop(); context.pop(true);
showBottomSheetWithBlur( showBottomSheetWithBlur(
userRootNavigator: true, userRootNavigator: true,
context: context, context: context,
@@ -241,7 +241,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else { } else {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return; if (!context.mounted) return;
context.pop(); context.pop(true);
showBottomSheetWithBlur( showBottomSheetWithBlur(
context: context, context: context,
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true), child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета', isDrafted: true),
@@ -304,7 +304,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
} else { } else {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return; if (!context.mounted) return;
context.pop(); context.pop(true);
showBottomSheetWithBlur( showBottomSheetWithBlur(
userRootNavigator: true, userRootNavigator: true,
context: context, context: context,

View File

@@ -13,9 +13,9 @@ import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
class CreateDietCubit extends Cubit<CreateDietState> { class CreateDietCubit extends Cubit<CreateDietState> {
@@ -29,9 +29,10 @@ class CreateDietCubit extends Cubit<CreateDietState> {
Future<void> setInitialData(Diet? diet) async { Future<void> setInitialData(Diet? diet) async {
if (diet == null) return; if (diet == null) return;
emit(state.copyWith(isLoading: true, isDietBeingEdited: true)); try {
dietId = diet.id; emit(state.copyWith(isLoading: true, isDietBeingEdited: true));
final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id); dietId = diet.id;
final dailyScheduleWithNutrition = await createDietRepository.getDietPortionsNutrition(diet.id);
final countOfDays = diet.duration; final countOfDays = diet.duration;
final List<List<List<DailySchedule>>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]); final List<List<List<DailySchedule>>> dailySchedule = List.generate(countOfDays, (index) => [[], [], [], [], []]);
@@ -66,7 +67,11 @@ class CreateDietCubit extends Cubit<CreateDietState> {
imageUrl: appConfig.buildType == AppBuildType.tests ? null : imageUrl, imageUrl: appConfig.buildType == AppBuildType.tests ? null : imageUrl,
), ),
); );
emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration))); emit(state.copyWith(isLoading: false, isDietBeingEdited: false, dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
} catch (e, st) {
await reportFrom(e, st, this);
emit(state.copyWith(isLoading: false, isDietBeingEdited: false));
}
} }
void setDetails(String text) { void setDetails(String text) {
@@ -112,7 +117,12 @@ class CreateDietCubit extends Cubit<CreateDietState> {
} }
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async { Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query); try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
} }
void setImage(XFile? image, {String? imageUrl}) { void setImage(XFile? image, {String? imageUrl}) {
@@ -204,7 +214,8 @@ class CreateDietCubit extends Cubit<CreateDietState> {
Future<Diet?> getDietForPreview() async { Future<Diet?> getDietForPreview() async {
if (!_isFormValidForDraft()) return null; if (!_isFormValidForDraft()) return null;
final authorProfile = await profileRepository.getMyProfile(); try {
final authorProfile = await profileRepository.getMyProfile();
final imageBytes = (await state.dietData.image?.readAsBytes()) ?? <int>[]; final imageBytes = (await state.dietData.image?.readAsBytes()) ?? <int>[];
String image = ''; String image = '';
if (imageBytes.isNotEmpty && imageBytes.length <= _maxImageBytesForBase64) { if (imageBytes.isNotEmpty && imageBytes.length <= _maxImageBytesForBase64) {
@@ -297,19 +308,26 @@ class CreateDietCubit extends Cubit<CreateDietState> {
isFavorite: false, isFavorite: false,
dailyNutrition: dailyNutrition, dailyNutrition: dailyNutrition,
); );
return diet; return diet;
} catch (e, st) {
await reportFrom(e, st, this);
return null;
}
} }
Future<File> downloadImageAsFile(String imageUrl) async { Future<File?> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl)); try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) { if (response.statusCode != 200) {
throw Exception('Не удалось загрузить изображение');
}
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}'; final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath); final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes); return file.writeAsBytes(response.bodyBytes);
} else { } catch (e, st) {
throw Exception('Не удалось загрузить изображение'); await reportFrom(e, st, this);
return null;
} }
} }
@@ -353,7 +371,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
emit(state.copyWith(published: dietState != DietState.draft, drafted: dietState == DietState.draft)); emit(state.copyWith(published: dietState != DietState.draft, drafted: dietState == DietState.draft));
return null; return null;
} catch (e, st) { } catch (e, st) {
await Sentry.captureException(e, stackTrace: st); await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit(state.copyWith(isDietPublishing: false)); emit(state.copyWith(isDietPublishing: false));
} }
@@ -391,7 +409,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
emit(state.copyWith(isDietBeingEdited: false)); emit(state.copyWith(isDietBeingEdited: false));
return null; return null;
} catch (e, st) { } catch (e, st) {
await Sentry.captureException(e, stackTrace: st); await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
emit(state.copyWith(isDietBeingEdited: false)); emit(state.copyWith(isDietBeingEdited: false));
return 'error'; return 'error';

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CreateDietRepository { class CreateDietRepository {
final CreateDietService createDietService; final CreateDietService createDietService;
@@ -62,48 +61,40 @@ class CreateDietRepository {
dietMap.addAll({'name': name, 'duration': duration}); dietMap.addAll({'name': name, 'duration': duration});
final formData = FormData.fromMap(dietMap); final formData = FormData.fromMap(dietMap);
try { switch (state) {
switch (state) { case DietState.active:
case DietState.active: return await createDietService.createPublishedDiet(formData: formData);
return await createDietService.createPublishedDiet(formData: formData); case DietState.draft:
case DietState.draft: return await createDietService.createDraftedDiet(formData: formData);
return await createDietService.createDraftedDiet(formData: formData); case DietState.personal:
case DietState.personal: return await createDietService.createPersonalDiet(formData: formData);
return await createDietService.createPersonalDiet(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
Future<Map<int, Map<int, Map<int, DailySchedule>>>> getDietPortionsNutrition(int dietId) async { Future<Map<int, Map<int, Map<int, DailySchedule>>>> getDietPortionsNutrition(int dietId) async {
try { final response = await createDietService.getDietPortionsNutrition(diet: dietId);
final response = await createDietService.getDietPortionsNutrition(diet: dietId); final data = response.data;
final data = response.data; final dailySchedule = (data as Map<String, dynamic>).map((key, value) {
final dailySchedule = (data as Map<String, dynamic>).map((key, value) { return MapEntry(
return MapEntry( int.parse(key),
int.parse(key), (value as Map<String, dynamic>).map((key, value) {
(value as Map<String, dynamic>).map((key, value) { return MapEntry(
return MapEntry( int.parse(key),
int.parse(key), (value as Map<String, dynamic>).map((key, value) {
(value as Map<String, dynamic>).map((key, value) { final portionMap = Map<String, dynamic>.from(value as Map<String, dynamic>);
final portionMap = Map<String, dynamic>.from(value as Map<String, dynamic>); final pnvid = portionMap['product_nutritional_value_id'];
final pnvid = portionMap['product_nutritional_value_id']; if (pnvid is String) {
if (pnvid is String) { portionMap['product_nutritional_value_id'] = int.tryParse(pnvid);
portionMap['product_nutritional_value_id'] = int.tryParse(pnvid); }
} return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap));
return MapEntry(int.parse(key), DailySchedule.fromJson(portionMap)); }),
}), );
); }),
}), );
); });
}); return dailySchedule;
return dailySchedule;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<int> updateDiet({ Future<int> updateDiet({
@@ -161,23 +152,16 @@ class CreateDietRepository {
} }
final formData = FormData.fromMap(dietMap); final formData = FormData.fromMap(dietMap);
try { switch (state) {
switch (state) { case DietState.active:
case DietState.active: final response = await createDietService.updatePublished(diet: dietId, formData: formData);
final response = await createDietService.updatePublished(diet: dietId, formData: formData); return response.response.statusCode ?? 0;
return response.response.statusCode ?? 0; case DietState.draft:
case DietState.draft: final response = await createDietService.updateDrafted(diet: dietId, formData: formData);
final response = await createDietService.updateDrafted(diet: dietId, formData: formData); return response.response.statusCode ?? 0;
return response.response.statusCode ?? 0; case DietState.personal:
case DietState.personal: final response = await createDietService.updatePersonal(diet: dietId, formData: formData);
final response = await createDietService.updatePersonal(diet: dietId, formData: formData); return response.response.statusCode ?? 0;
return response.response.statusCode ?? 0;
}
} on DioException catch (e) {
return e.response?.statusCode ?? 0;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
} }

View File

@@ -7,6 +7,7 @@ import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart'; import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
class CreatePlanCubit extends Cubit<CreatePlanState> { class CreatePlanCubit extends Cubit<CreatePlanState> {
@@ -457,7 +458,8 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
await createPlanRepository.createPlan(model: planMap); await createPlanRepository.createPlan(model: planMap);
emit(state.copyWith(isLoading: false, planCreated: true)); emit(state.copyWith(isLoading: false, planCreated: true));
} }
} catch (e) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString()); log(e.toString());
emit(state.copyWith(isLoading: false, planCreated: true)); emit(state.copyWith(isLoading: false, planCreated: true));
} }

View File

@@ -1,6 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class CreatePlanRepository { class CreatePlanRepository {
final CreatePlanService createPlanService; final CreatePlanService createPlanService;
@@ -10,13 +9,9 @@ class CreatePlanRepository {
Future<void> createPlan({ Future<void> createPlan({
required Map<String, dynamic> model, required Map<String, dynamic> model,
}) async { }) async {
try { final formData = FormData.fromMap(model);
final formData = FormData.fromMap(model); return await createPlanService.createPlan(formData: formData);
return await createPlanService.createPlan(formData: formData);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -11,7 +11,7 @@ import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/glossary/models/glossary_model.dart'; import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:eatvio_app/core/error_reporting/report_from.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -166,7 +166,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
await recipeRepository.publishRecipe(model, update, slug); await recipeRepository.publishRecipe(model, update, slug);
return true; return true;
} catch (e, st) { } catch (e, st) {
await Sentry.captureException(e, stackTrace: st); await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
return false; return false;
} }
@@ -179,7 +179,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
emit(state.copyWith(drafted: true)); emit(state.copyWith(drafted: true));
return true; return true;
} catch (e, st) { } catch (e, st) {
await Sentry.captureException(e, stackTrace: st); await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
return false; return false;
} }
@@ -204,16 +204,19 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
)); ));
} }
Future<File> downloadImageAsFile(String imageUrl) async { Future<File?> downloadImageAsFile(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl)); try {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) { if (response.statusCode != 200) {
throw Exception('Не удалось загрузить изображение');
}
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${imageUrl.split('/').last}'; final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
final file = File(filePath); final file = File(filePath);
return await file.writeAsBytes(response.bodyBytes); return file.writeAsBytes(response.bodyBytes);
} else { } catch (e, st) {
throw Exception('Не удалось загрузить изображение'); await reportFrom(e, st, this);
return null;
} }
} }
@@ -222,44 +225,53 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
} }
Future<void> recipeToEntity(Recipe recipe) async { Future<void> recipeToEntity(Recipe recipe) async {
final file = await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${recipe.image}'); try {
final file = await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${recipe.image}');
if (file == null) {
disableLoading();
return;
}
final List<StepEntity> steps = await Future.wait( final List<StepEntity> steps = await Future.wait(
recipe.steps!.map( recipe.steps!.map(
(e) async => StepEntity( (e) async => StepEntity(
description: e.description, description: e.description,
image: e.image.isEmpty ? null : await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${e.image}'), image: e.image.isEmpty ? null : await downloadImageAsFile('${sl<AppConfig>().baseUrl}/storage/${e.image}'),
),
), ),
), );
); emit(
emit( CreateRecipeState(
CreateRecipeState( entity: RecipeCreateEntity(
entity: RecipeCreateEntity( title: recipe.title,
title: recipe.title, typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!),
typeOfRecipe: TypeOfRecipe.fromApiType(recipe.type!), countOfPortions: recipe.servings,
countOfPortions: recipe.servings, energyInOnePortion: recipe.calories,
energyInOnePortion: recipe.calories, totalCookHours: recipe.cookingTime ~/ 60,
totalCookHours: recipe.cookingTime ~/ 60, totalCookMinutes: recipe.cookingTime % 60,
totalCookMinutes: recipe.cookingTime % 60, activeCookHours: recipe.activeCookingTime ~/ 60,
activeCookHours: recipe.activeCookingTime ~/ 60, activeCookMinutes: recipe.activeCookingTime % 60,
activeCookMinutes: recipe.activeCookingTime % 60, description: recipe.description,
description: recipe.description, image: file,
image: file, videoUrl: recipe.videoUrl,
videoUrl: recipe.videoUrl, ingredients: recipe.ingredients!
ingredients: recipe.ingredients! .map((e) => i.Ingredient(
.map((e) => i.Ingredient( name: e.name,
name: e.name, unit: e.unit!,
unit: e.unit!, portion: e.portion,
portion: e.portion, productId: e.productId,
productId: e.productId, ))
)) .toList(),
.toList(), steps: steps,
steps: steps, tags: recipe.tags.map((e) => (e.name)).toList(),
tags: recipe.tags.map((e) => (e.name)).toList(), ),
loading: false,
), ),
loading: false, );
), } catch (e, st) {
); await reportFrom(e, st, this);
disableLoading();
}
} }
bool validateAll() { bool validateAll() {
@@ -376,13 +388,18 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
await recipeRepository.deleteRecipe(slug); await recipeRepository.deleteRecipe(slug);
return true; return true;
} catch (e, s) { } catch (e, s) {
await Sentry.captureException(e, stackTrace: s); await reportFrom(e, s, this);
return false; return false;
} }
} }
Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async { Future<List<GlossaryModel>> getGlossaryListByQuery(String query) async {
return await glossaryRepository.getGlossaryListByQuery(query); try {
return await glossaryRepository.getGlossaryListByQuery(query);
} catch (e, st) {
await reportFrom(e, st, this);
return [];
}
} }
bool validateSteps() { bool validateSteps() {

View File

@@ -109,7 +109,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
if (isSuccess) { if (isSuccess) {
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
if (!context.mounted) return; if (!context.mounted) return;
context.pop(); context.pop('active');
showBottomSheetWithBlur( showBottomSheetWithBlur(
userRootNavigator: true, userRootNavigator: true,
context: context, context: context,
@@ -157,7 +157,7 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
}); });
if (isSuccess) { if (isSuccess) {
if (!context.mounted) return; if (!context.mounted) return;
context.pop(); context.pop('drafted');
} else { } else {
if (!context.mounted) return; if (!context.mounted) return;
showErrorDialog(context, title: 'Не удалось сохранить черновик'); showErrorDialog(context, title: 'Не удалось сохранить черновик');

View File

@@ -7,7 +7,6 @@ import 'package:eatvio_app/features/create_recipe/models/recipe_model.dart';
import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart'; import 'package:eatvio_app/features/create_recipe/models/recipe_tag_model.dart';
import 'package:eatvio_app/features/create_recipe/models/step.dart'; import 'package:eatvio_app/features/create_recipe/models/step.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:eatvio_app/features/create_recipe/models/product.dart'; import 'package:eatvio_app/features/create_recipe/models/product.dart';
class RecipeRepository { class RecipeRepository {
@@ -16,12 +15,8 @@ class RecipeRepository {
RecipeRepository({required this.recipeService}); RecipeRepository({required this.recipeService});
Future<Map<String, List<Product>>> inputProduct(String name) async { Future<Map<String, List<Product>>> inputProduct(String name) async {
try { return await recipeService.getInputProduct(name);
return await recipeService.getInputProduct(name);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List> _buildStepsMap(List<Step?>? steps) async { Future<List> _buildStepsMap(List<Step?>? steps) async {
@@ -110,67 +105,43 @@ class RecipeRepository {
} }
Future<void> publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async { Future<void> publishRecipe(RecipeModel recipe, [bool update = false, String? slug]) async {
try { final formData = await _buildPublishFormData(recipe);
final formData = await _buildPublishFormData(recipe); if (update) {
if (update) { await recipeService.updateRecipe(slug: slug!, formData: formData);
await recipeService.updateRecipe(slug: slug!, formData: formData); } else {
} else { await recipeService.publishRecipe(formData: formData);
await recipeService.publishRecipe(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
Future<void> saveAsDraft(RecipeModel recipe, [bool update = false, String? slug]) async { Future<void> saveAsDraft(RecipeModel recipe, [bool update = false, String? slug]) async {
try { final formData = await _buildDraftFormData(recipe);
final formData = await _buildDraftFormData(recipe); log(formData.fields.toString());
log(formData.fields.toString()); if (update) {
if (update) { await recipeService.updateRecipe(slug: slug!, formData: formData);
await recipeService.updateRecipe(slug: slug!, formData: formData); } else {
} else { await recipeService.createDraftedRecipe(formData: formData);
await recipeService.createDraftedRecipe(formData: formData);
}
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
} }
} }
Future<void> draftExistingRecipe(int id) async { Future<void> draftExistingRecipe(int id) async {
try { await recipeService.draftRecipe(id: id);
await recipeService.draftRecipe(id: id);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<RecipeModel> getRecipe(String recipeID) async { Future<RecipeModel> getRecipe(String recipeID) async {
try { return await recipeService.getRecipe(recipeID);
return await recipeService.getRecipe(recipeID);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<List<RecipeTag>> getRecipesTags() async { Future<List<RecipeTag>> getRecipesTags() async {
try { return (await recipeService.getTags()).data;
return (await recipeService.getTags()).data;
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
Future<void> deleteRecipe(String slug) async { Future<void> deleteRecipe(String slug) async {
try { return await recipeService.deleteRecipe(slug: slug);
return await recipeService.deleteRecipe(slug: slug);
} catch (e, s) {
await Sentry.captureException(e, stackTrace: s);
rethrow;
}
} }
} }

View File

@@ -240,7 +240,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
} else { } else {
platform = 'ios'; platform = 'ios';
} }
await dio.post('/api/profile/notification/token/register', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); await dio.post('/api/profile/notification/token/register', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен добавлен'); showInfoToast('Токен добавлен');
} catch (e) { } catch (e) {
showInfoToast('Не удалось добавть токен'); showInfoToast('Не удалось добавть токен');
@@ -253,7 +253,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
buttonStyle: const ButtonStyle(), buttonStyle: const ButtonStyle(),
onPressed: () async { onPressed: () async {
try { try {
final token = await FirebaseApi().getToken(); final token = await sl<FirebaseApi>().getToken();
if (token != null) { if (token != null) {
Clipboard.setData(ClipboardData(text: token)); Clipboard.setData(ClipboardData(text: token));
showInfoToast('Токен скопирован'); showInfoToast('Токен скопирован');
@@ -278,7 +278,7 @@ class _DevToolsScreenContentState extends State<DevToolsScreenContent> {
} else { } else {
platform = 'ios'; platform = 'ios';
} }
await dio.post('/api/profile/notification/token/unregister', data: {'token': await FirebaseApi().getToken(), 'platform': platform}); await dio.post('/api/profile/notification/token/unregister', data: {'token': await sl<FirebaseApi>().getToken(), 'platform': platform});
showInfoToast('Токен удалён'); showInfoToast('Токен удалён');
} catch (e) { } catch (e) {
showInfoToast('Не удалось удалить токен'); showInfoToast('Не удалось удалить токен');

View File

@@ -36,7 +36,6 @@ import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/env_config.dart';
class TestResult { class TestResult {
final bool success; final bool success;
final String? error; final String? error;
@@ -57,7 +56,9 @@ class CreateRecipeTask implements TestCases {
Future<TestResult> run({required String baseUrl}) async { Future<TestResult> run({required String baseUrl}) async {
try { try {
final dio = sl<Dio>(); final dio = sl<Dio>();
final recipeRepository = RecipeRepository(recipeService: CreateRecipeService(dio, baseUrl: baseUrl)); final recipeRepository = RecipeRepository(
recipeService: CreateRecipeService(dio, baseUrl: baseUrl),
);
await recipeRepository.publishRecipe( await recipeRepository.publishRecipe(
const RecipeModel( const RecipeModel(
title: 'Title', title: 'Title',

View File

@@ -3,6 +3,7 @@ import 'package:eatvio_app/features/diary/models/consultant_comment_model.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_state.dart';
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
class ConsultantCommentsCubit extends Cubit<ConsultantCommentsState> { class ConsultantCommentsCubit extends Cubit<ConsultantCommentsState> {
final DiaryRepository diaryRepository; final DiaryRepository diaryRepository;
@@ -74,6 +75,7 @@ class ConsultantCommentsCubit extends Cubit<ConsultantCommentsState> {
} }
await diaryRepository.commentUserDay(diaryId: diaryId, commentModel: state.commentsModel); await diaryRepository.commentUserDay(diaryId: diaryId, commentModel: state.commentsModel);
} catch (e, st) { } catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
} }
} }

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