forked from eatvio/eatvio_app
Compare commits
251 Commits
issue-795
...
push-notif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8284338aa | ||
|
|
03ec91a595 | ||
|
|
e814683984 | ||
|
|
684415ef5b | ||
|
|
631ceb2b7a | ||
|
|
71a405c63f | ||
|
|
4ca90eb2df | ||
|
|
f17f1ecb57 | ||
|
|
a71bcc752d | ||
|
|
aac085def4 | ||
|
|
b6b13b1db5 | ||
|
|
6629286bf1 | ||
|
|
966bc23f0b | ||
|
|
a13b623ca2 | ||
|
|
ee2dd75a94 | ||
|
|
c450a41715 | ||
|
|
55b6b75134 | ||
|
|
335e36dd18 | ||
| 7106bde4ae | |||
|
|
4f241f7db6 | ||
|
|
3d7755a9a2 | ||
|
|
decc8c63e6 | ||
|
|
ad5b48ef1b | ||
|
|
e8dc0138e2 | ||
|
|
96896671b6 | ||
|
|
5e2f0430d9 | ||
|
|
2da3b10e63 | ||
|
|
5cb121d733 | ||
|
|
109013c25c | ||
|
|
f5d14d213f | ||
|
|
26ae28a705 | ||
|
|
7672477c03 | ||
|
|
07277d6e48 | ||
|
|
9d1bedff26 | ||
|
|
e39638be7f | ||
| 442fc0ec14 | |||
| a8f8cc8eb0 | |||
|
|
3892bf00aa | ||
|
|
a492c2d964 | ||
|
|
89268e1132 | ||
| 60753a13d2 | |||
|
|
08eeea6ba4 | ||
|
|
58d99424a0 | ||
|
|
c6cdb5bf01 | ||
|
|
50deaeddd1 | ||
|
|
56fb2f1bac | ||
|
|
24185cc875 | ||
|
|
9e7bf258d0 | ||
|
|
098633b3ac | ||
|
|
c3aa77b02d | ||
|
|
2d5384f2ec | ||
|
|
1c7e494f90 | ||
|
|
8bbe2613d2 | ||
|
|
5f6ceab6a1 | ||
|
|
250a3f5d73 | ||
|
|
3e0d8b6c27 | ||
|
|
9ed3b37272 | ||
|
|
ae628a6ecc | ||
|
|
88d8ec52d9 | ||
|
|
937921e26f | ||
|
|
d324bd6540 | ||
|
|
5766e216c8 | ||
|
|
ee9b97b694 | ||
|
|
31eec6c9d7 | ||
|
|
5fae3eb5e8 | ||
|
|
b0a167b9e9 | ||
|
|
0b2e026d1d | ||
|
|
0641513ebe | ||
|
|
be0a99b164 | ||
|
|
8b1d08b2aa | ||
|
|
6dab0d159b | ||
|
|
5167766dbf | ||
| cc79e40df9 | |||
|
|
cdc5936d3c | ||
|
|
67e770f5d0 | ||
|
|
6f666a8859 | ||
|
|
c540cbe6df | ||
|
|
6c4104e9ea | ||
|
|
6d1bcb85fb | ||
|
|
9753644570 | ||
|
|
75983818d1 | ||
|
|
5b149e2fd2 | ||
|
|
699148b202 | ||
|
|
6e7894b911 | ||
|
|
d4f41fe6cf | ||
|
|
bb97c395dd | ||
|
|
b7804db49d | ||
|
|
e90a466a5b | ||
|
|
5f30aaeb2a | ||
|
|
516dfa4158 | ||
|
|
ae07851396 | ||
|
|
34eccb7db5 | ||
|
|
042cc0ea20 | ||
|
|
f81aebe9d8 | ||
|
|
d70aaf955f | ||
|
|
c91e83adaf | ||
|
|
531f732b1a | ||
|
|
d10f2f1356 | ||
|
|
bf110b70c1 | ||
|
|
97418e11d4 | ||
|
|
225077907f | ||
|
|
296adc8229 | ||
|
|
7dd7e3b488 | ||
|
|
a496edaf4c | ||
|
|
400cc004ef | ||
|
|
beda152df0 | ||
|
|
42ffde820d | ||
|
|
adb7b0bd74 | ||
|
|
a768eefed0 | ||
|
|
0ac50d6d18 | ||
|
|
a0d8fda50d | ||
|
|
1d286fa8d9 | ||
|
|
c2a5e7305e | ||
|
|
bbb1b28692 | ||
|
|
21a65caea9 | ||
|
|
eac366e9ae | ||
|
|
278f4d7e10 | ||
|
|
ac5504a514 | ||
|
|
843e1ce4dd | ||
|
|
db6db89567 | ||
|
|
044d86e710 | ||
|
|
4ff880e079 | ||
|
|
05b9ca17c7 | ||
|
|
a1c0c2694b | ||
|
|
2d7407ce99 | ||
| 62fe5ebfa0 | |||
|
|
3dc36ab52d | ||
| 190e212eb4 | |||
|
|
cf3d1a945c | ||
|
|
c54915f45d | ||
|
|
b188570882 | ||
|
|
d0794670f6 | ||
|
|
3b53ed6f7d | ||
|
|
a003814265 | ||
|
|
e99465a550 | ||
|
|
a01f3914c4 | ||
|
|
15f9149cfd | ||
|
|
79df8c5f9a | ||
|
|
a93da1b505 | ||
|
|
8144013747 | ||
|
|
7bac2a13ac | ||
|
|
29589ef6fd | ||
|
|
97a6fe57e9 | ||
|
|
8309498d9e | ||
|
|
aa098b31bc | ||
|
|
7a85c65918 | ||
|
|
6ff35fd162 | ||
|
|
6811b325be | ||
|
|
92205c09d6 | ||
| 008dba9fed | |||
|
|
55ca558bfb | ||
|
|
91499ae207 | ||
|
|
4bacdf5e76 | ||
|
|
e76c1aed20 | ||
|
|
de8f879f59 | ||
|
|
f6809a06b3 | ||
|
|
775d4fb80f | ||
|
|
64e9df69f7 | ||
|
|
6ccfcdedda | ||
|
|
d3c8034b8f | ||
|
|
44ede73ceb | ||
| 2c63990afc | |||
|
|
eb69b253bc | ||
|
|
f26886a7c5 | ||
|
|
13ccf56df0 | ||
|
|
c0032f7a95 | ||
|
|
4852d2cf10 | ||
|
|
41e96c63c6 | ||
|
|
c786821275 | ||
|
|
7b425e56f1 | ||
|
|
bf2ba1fbda | ||
|
|
d206e7ec12 | ||
|
|
a778257215 | ||
|
|
497ff19cfa | ||
|
|
bb59552e3e | ||
|
|
55b2bb0436 | ||
|
|
90746d0be7 | ||
|
|
d70d172c69 | ||
|
|
4e23992d22 | ||
|
|
7a846ed218 | ||
|
|
fcc2936c22 | ||
|
|
b11cf2c0b0 | ||
|
|
0ead14bca5 | ||
|
|
08e56d9777 | ||
|
|
99659f7ea1 | ||
|
|
e158d812e4 | ||
|
|
3d774267ca | ||
|
|
aeb89a9aba | ||
|
|
a4f5bfdb4d | ||
|
|
5b705dfb72 | ||
|
|
a792dcd163 | ||
|
|
9c7ca14017 | ||
|
|
38118055a5 | ||
|
|
5d6e25858b | ||
|
|
42b443c3ba | ||
|
|
21f7c40cde | ||
|
|
f56092d88f | ||
|
|
2b97c0f57d | ||
|
|
a1304e4612 | ||
|
|
7b534cf891 | ||
|
|
22b2e5f249 | ||
|
|
5dfdbe4543 | ||
|
|
5c9a9c22ba | ||
|
|
4f920359c8 | ||
|
|
2ab3bc1cf0 | ||
|
|
91bd8f9d68 | ||
| 9c6f6bdfb1 | |||
|
|
fe1e81e455 | ||
|
|
b7e064a560 | ||
|
|
f1b7a24044 | ||
|
|
cb441b4027 | ||
| 7282fea59a | |||
|
|
4adec4dbbc | ||
|
|
d5b0e972a5 | ||
|
|
391efeb8dd | ||
| d115af6f8e | |||
|
|
2408443a26 | ||
|
|
c378c7fc16 | ||
|
|
070a3e39e7 | ||
|
|
d1753e99f8 | ||
|
|
960afbd6ed | ||
|
|
70e4a42e98 | ||
|
|
04061cb4c1 | ||
|
|
edd064d20d | ||
|
|
cdb7378265 | ||
|
|
746d366c18 | ||
|
|
c2a506cdbf | ||
|
|
abe4985f0e | ||
|
|
ca06b2bd0f | ||
|
|
04c932646d | ||
|
|
3d4363185b | ||
|
|
953230b387 | ||
|
|
9d845e1264 | ||
|
|
32de60742e | ||
|
|
3923b61c67 | ||
|
|
0cd8e662d9 | ||
| 89ea73fc59 | |||
|
|
f3d4478c65 | ||
|
|
d0becb9e11 | ||
|
|
965ae2f02b | ||
|
|
9fcfac87d4 | ||
|
|
2a25be5aa4 | ||
|
|
7df32dabd2 | ||
|
|
2e57cd9519 | ||
|
|
58bb766721 | ||
|
|
0be7392a39 | ||
|
|
4deda92c5e | ||
|
|
6fa944b3df | ||
|
|
c31411c4a2 | ||
|
|
6214d19a14 | ||
|
|
db5dddff86 |
@@ -3,19 +3,19 @@ name: Eatvio App Android Test
|
|||||||
on: [ pull_request ]
|
on: [ pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
flutter_version: "3.32.0"
|
flutter_version: "3.41.2"
|
||||||
java_version: "21.x"
|
java_version: "21.x"
|
||||||
flutter_channel: "stable"
|
flutter_channel: "stable"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: [ dev-office ]
|
runs-on: [ ubuntu-latest ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: https://github.com/actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: 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 }}
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Install Android SDK
|
- name: Install Android SDK
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y openjdk-11-jdk wget
|
apt-get install -y openjdk-11-jdk wget xz-utils tar
|
||||||
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
|
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
|
||||||
unzip commandlinetools.zip
|
unzip commandlinetools.zip
|
||||||
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
|
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
apt-get install -y jq
|
apt-get install -y jq
|
||||||
|
|
||||||
- id: flutter-action
|
- id: flutter-action
|
||||||
uses: actions/flutter-action@v2
|
uses: https://github.com/subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: ${{ env.flutter_channel }}
|
channel: ${{ env.flutter_channel }}
|
||||||
flutter-version: ${{ env.flutter_version }}
|
flutter-version: ${{ env.flutter_version }}
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload APK artifact
|
- name: Upload APK artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: https://github.com/actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.APK_NAME }}
|
name: ${{ env.APK_NAME }}
|
||||||
path: ${{ env.APK_NAME }}
|
path: ${{ env.APK_NAME }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker
|
|||||||
on: [ pull_request ]
|
on: [ pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
flutter_version: "3.32.0"
|
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/"
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: [ dev-office-flutter ]
|
runs-on: [ dev-office-flutter ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: https://github.com/actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Android environment variables
|
- name: Set up Android environment variables
|
||||||
id: "android-env-setup"
|
id: "android-env-setup"
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
# echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
# echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
# - name: Upload APK artifact
|
# - name: Upload APK artifact
|
||||||
# uses: actions/upload-artifact@v3
|
# uses: https://github.com/actions/upload-artifact@v3
|
||||||
# with:
|
# with:
|
||||||
# name: ${{ env.APK_NAME }}
|
# name: ${{ env.APK_NAME }}
|
||||||
# path: ${{ env.APK_NAME }}
|
# path: ${{ env.APK_NAME }}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ jobs:
|
|||||||
runs-on: [macos-arm64]
|
runs-on: [macos-arm64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: https://github.com/actions/checkout@v2
|
||||||
|
|
||||||
- name: Checkout env repository
|
- name: Checkout env repository
|
||||||
uses: actions/checkout@v2
|
uses: https://github.com/actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
repository: mso7nueh/eatvio_app_env
|
repository: mso7nueh/eatvio_app_env
|
||||||
path: env_repo
|
path: env_repo
|
||||||
@@ -54,11 +54,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Flutter build
|
- name: Flutter build
|
||||||
run: |
|
run: |
|
||||||
|
flutter upgrade
|
||||||
flutter clean
|
flutter clean
|
||||||
flutter pub get
|
flutter pub get
|
||||||
flutter pub upgrade
|
flutter pub upgrade
|
||||||
flutter pub run build_runner build --delete-conflicting-outputs
|
flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
# Configure Flutter for release mode
|
# Configure Flutter for release mode
|
||||||
|
pod repo update
|
||||||
flutter build ios --config-only --release
|
flutter build ios --config-only --release
|
||||||
cd ios
|
cd ios
|
||||||
pod update
|
pod update
|
||||||
|
|||||||
@@ -1,27 +1,39 @@
|
|||||||
name: Eatvio App Android Test
|
name: Eatvio App Android Test
|
||||||
|
|
||||||
on: [ pull_request ]
|
on: [pull_request]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
flutter_version: "3.35.1"
|
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: [ dev-office ]
|
runs-on: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: https://github.com/actions/checkout@v3
|
||||||
|
|
||||||
|
# Пока что не нужно проксирование
|
||||||
|
# - name: Set proxy server
|
||||||
|
# env:
|
||||||
|
# PROXY_USER: ${{ secrets.PROXY_USER }}
|
||||||
|
# PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
|
||||||
|
# run: |
|
||||||
|
# echo "Setting up proxy..."
|
||||||
|
# cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
|
||||||
|
# Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
|
||||||
|
# EOF
|
||||||
|
|
||||||
- name: Checkout env repository
|
- name: Checkout env repository
|
||||||
uses: 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
|
||||||
@@ -56,22 +68,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Cache Android SDK
|
- name: Cache Android SDK
|
||||||
id: cache-sdk
|
id: cache-sdk
|
||||||
uses: "https://gitea.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
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: 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-11-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'
|
||||||
@@ -93,22 +105,28 @@ jobs:
|
|||||||
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
|
||||||
|
|
||||||
- id: flutter-action
|
- name: Set up Flutter
|
||||||
uses: actions/flutter-action@v2
|
run: |
|
||||||
with:
|
FLUTTER_DIR="${{ env.my_local_cache_dir }}flutter"
|
||||||
channel: ${{ env.flutter_channel }}
|
if [ ! -f "$FLUTTER_DIR/bin/flutter" ]; then
|
||||||
flutter-version: ${{ env.flutter_version }}
|
echo "Downloading Flutter ${{ env.flutter_version }}..."
|
||||||
cache: true
|
curl -fsSL -o /tmp/flutter.tar.xz "https://storage.googleapis.com/flutter_infra_release/releases/${{ env.flutter_channel }}/linux/flutter_linux_${{ env.flutter_version }}-${{ env.flutter_channel }}.tar.xz"
|
||||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache
|
echo "Extracting Flutter..."
|
||||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
|
mkdir -p "${{ env.my_local_cache_dir }}"
|
||||||
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache of dart pub get dependencies
|
tar xf /tmp/flutter.tar.xz -C "${{ env.my_local_cache_dir }}"
|
||||||
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
|
rm /tmp/flutter.tar.xz
|
||||||
|
else
|
||||||
|
echo "Flutter already cached at $FLUTTER_DIR"
|
||||||
|
fi
|
||||||
|
echo "$FLUTTER_DIR/bin" >> $GITHUB_PATH
|
||||||
|
echo "$FLUTTER_DIR/bin/cache/dart-sdk/bin" >> $GITHUB_PATH
|
||||||
|
echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
# - name: Print flutter-action outputs
|
# - name: Print flutter-action outputs
|
||||||
# shell: bash
|
# shell: bash
|
||||||
@@ -126,7 +144,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Fix dir permissions
|
- name: Fix dir permissions
|
||||||
run: |
|
run: |
|
||||||
git config --global --add safe.directory /opt/hostedtoolcache/flutter/${{ env.flutter_channel }}-${{ env.flutter_version }}-${{ steps.flutter-action.outputs.ARCHITECTURE }}
|
ARCH=$(uname -m)
|
||||||
|
if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi
|
||||||
|
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
|
||||||
|
|
||||||
@@ -144,14 +164,61 @@ jobs:
|
|||||||
|
|
||||||
- name: Test coverage
|
- name: Test coverage
|
||||||
run: |
|
run: |
|
||||||
|
echo "📊 Analyzing test coverage..."
|
||||||
|
echo ""
|
||||||
awk -F: '
|
awk -F: '
|
||||||
/^LF:/ { total += $2 }
|
/^SF:/ {
|
||||||
/^LH:/ { covered += $2 }
|
file = $0
|
||||||
END {
|
sub(/^SF:/, "", file)
|
||||||
if (total == 0) {
|
sub(/^[[:space:]]+/, "", file)
|
||||||
print "Coverage: 0%"
|
is_generated = 0
|
||||||
|
if (file ~ /\.(g|freezed|gr|mocks)\.dart$/) {
|
||||||
|
is_generated = 1
|
||||||
|
generated_files++
|
||||||
|
} else if (file ~ /\/(build|\.dart_tool)\//) {
|
||||||
|
is_generated = 1
|
||||||
|
generated_files++
|
||||||
} else {
|
} else {
|
||||||
printf "Coverage: %.2f%%\n", (covered / total) * 100
|
source_files++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^LF:/ {
|
||||||
|
if (!is_generated) {
|
||||||
|
total += $2
|
||||||
|
} else {
|
||||||
|
generated_lines += $2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^LH:/ {
|
||||||
|
if (!is_generated) {
|
||||||
|
covered += $2
|
||||||
|
} else {
|
||||||
|
generated_covered += $2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END {
|
||||||
|
print "📁 Files:"
|
||||||
|
printf " Source files: %d\n", source_files
|
||||||
|
printf " Generated files: %d (excluded)\n", generated_files
|
||||||
|
print ""
|
||||||
|
print "📝 Lines:"
|
||||||
|
printf " Total lines: %d\n", total
|
||||||
|
printf " Covered lines: %d\n", covered
|
||||||
|
printf " Uncovered lines: %d\n", total - covered
|
||||||
|
print ""
|
||||||
|
if (total == 0) {
|
||||||
|
print "⚠️ Coverage: 0%"
|
||||||
|
exit 1
|
||||||
|
} else {
|
||||||
|
percentage = (covered / total) * 100
|
||||||
|
printf "✅ Coverage: %.2f%%\n", percentage
|
||||||
|
print ""
|
||||||
|
if (percentage < 70) {
|
||||||
|
printf "❌ FAIL: Coverage is below 70%%\n"
|
||||||
|
exit 1
|
||||||
|
} else {
|
||||||
|
printf "💚 Excellent: Coverage is above 70%%\n"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
' coverage/lcov.info
|
' coverage/lcov.info
|
||||||
@@ -172,7 +239,7 @@ jobs:
|
|||||||
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Upload APK artifact
|
- name: Upload APK artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: https://github.com/actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.APK_NAME }}
|
name: ${{ env.APK_NAME }}
|
||||||
path: ${{ env.APK_NAME }}
|
path: ${{ env.APK_NAME }}
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -20,3 +20,8 @@ env/.env
|
|||||||
.env
|
.env
|
||||||
android/key.properties
|
android/key.properties
|
||||||
app/upload-keystore.jks
|
app/upload-keystore.jks
|
||||||
|
coverage/
|
||||||
|
backend/
|
||||||
|
.fvm/
|
||||||
|
.fvmrc
|
||||||
|
/eatvio
|
||||||
55
analyze_coverage.py
Normal file
55
analyze_coverage.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
|
||||||
|
def analyze_coverage():
|
||||||
|
files = {}
|
||||||
|
current_file = None
|
||||||
|
|
||||||
|
with open('coverage/lcov.info', 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i].strip()
|
||||||
|
if line.startswith('SF:'):
|
||||||
|
current_file = line[3:]
|
||||||
|
files[current_file] = {'total': 0, 'covered': 0}
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LF:') and current_file:
|
||||||
|
files[current_file]['total'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LH:') and current_file:
|
||||||
|
files[current_file]['covered'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Filter out generated files
|
||||||
|
filtered = []
|
||||||
|
for f, d in files.items():
|
||||||
|
if not f.endswith('.g.dart') and not 'generated' in f.lower() and not f.endswith('l10n.dart'):
|
||||||
|
filtered.append((f, d))
|
||||||
|
|
||||||
|
# Find files with less than 50% coverage
|
||||||
|
low_coverage = []
|
||||||
|
for f, d in filtered:
|
||||||
|
if d['total'] > 0:
|
||||||
|
pct = d['covered']/d['total']*100
|
||||||
|
if pct < 50:
|
||||||
|
low_coverage.append((f, d))
|
||||||
|
|
||||||
|
# Sort by coverage percentage (lowest first)
|
||||||
|
sorted_files = sorted(
|
||||||
|
low_coverage,
|
||||||
|
key=lambda x: (x[1]['covered']/x[1]['total']*100 if x[1]['total'] > 0 else 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
print('Files with less than 50% coverage:')
|
||||||
|
print('=' * 80)
|
||||||
|
for f, d in sorted_files[:50]:
|
||||||
|
pct = d['covered']/d['total']*100
|
||||||
|
print(f'{pct:6.1f}% ({d["covered"]:4d}/{d["total"]:4d}) - {f}')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
analyze_coverage()
|
||||||
|
|
||||||
52
analyze_coverage_by_uncovered.py
Normal file
52
analyze_coverage_by_uncovered.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
|
||||||
|
def analyze_coverage_by_uncovered():
|
||||||
|
files = {}
|
||||||
|
current_file = None
|
||||||
|
|
||||||
|
with open('coverage/lcov.info', 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i].strip()
|
||||||
|
if line.startswith('SF:'):
|
||||||
|
current_file = line[3:]
|
||||||
|
files[current_file] = {'total': 0, 'covered': 0}
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LF:') and current_file:
|
||||||
|
files[current_file]['total'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('LH:') and current_file:
|
||||||
|
files[current_file]['covered'] = int(line[3:])
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Filter out generated files and UI screens/widgets (hard to test)
|
||||||
|
filtered = []
|
||||||
|
exclude_patterns = ['.g.dart', 'generated', 'l10n.dart', '/pages/', '/widgets/', '_screen.dart', '_widget.dart']
|
||||||
|
|
||||||
|
for f, d in files.items():
|
||||||
|
if d['total'] > 0:
|
||||||
|
# Skip UI files
|
||||||
|
should_exclude = any(pattern in f for pattern in exclude_patterns)
|
||||||
|
if not should_exclude:
|
||||||
|
uncovered = d['total'] - d['covered']
|
||||||
|
pct = d['covered']/d['total']*100
|
||||||
|
filtered.append((f, d, uncovered, pct))
|
||||||
|
|
||||||
|
# Sort by uncovered lines (most uncovered first)
|
||||||
|
sorted_files = sorted(filtered, key=lambda x: x[2], reverse=True)
|
||||||
|
|
||||||
|
print('Files sorted by uncovered lines (excluding UI screens/widgets):')
|
||||||
|
print('=' * 100)
|
||||||
|
print(f'{"Uncovered":>10} {"Covered":>8} {"Total":>8} {"%":>7} {"File":<60}')
|
||||||
|
print('=' * 100)
|
||||||
|
for f, d, uncovered, pct in sorted_files[:30]:
|
||||||
|
print(f'{uncovered:>10} {d["covered"]:>8} {d["total"]:>8} {pct:>6.1f}% {f}')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
analyze_coverage_by_uncovered()
|
||||||
|
|
||||||
52
analyze_output.txt
Normal file
52
analyze_output.txt
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
Files with less than 50% coverage:
|
||||||
|
================================================================================
|
||||||
|
0.0% ( 0/ 37) - lib/core/firebase_api.dart
|
||||||
|
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_confirm_request.dart
|
||||||
|
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_confirm_response.dart
|
||||||
|
0.0% ( 0/ 1) - lib/features/auth/models/oauth_verification_request_response.dart
|
||||||
|
0.0% ( 0/ 342) - lib/features/dev_tools/test_cases.dart
|
||||||
|
0.9% ( 2/ 220) - lib/features/dev_tools/presentation/dev_tools_screen.dart
|
||||||
|
2.4% ( 1/ 42) - lib/features/auth/presentation/pages/scan_qr_code_screen.dart
|
||||||
|
2.9% ( 1/ 34) - lib/features/diary/presentation/pages/barcode_scanner_screen.dart
|
||||||
|
3.6% ( 1/ 28) - lib/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart
|
||||||
|
3.8% ( 1/ 26) - lib/features/update_screen/presentation/update_screen.dart
|
||||||
|
4.4% ( 4/ 90) - lib/core/widgets/custom_popup.dart
|
||||||
|
5.9% ( 1/ 17) - lib/features/recipe/presentation/pages/my_recipes_screen.dart
|
||||||
|
6.2% ( 2/ 32) - lib/features/consultations/presentation/pages/consultations_screen.dart
|
||||||
|
7.1% ( 1/ 14) - lib/features/menu/presentation/pages/blocked_users_screen.dart
|
||||||
|
7.7% ( 1/ 13) - lib/features/registration_v2/presentation/pages/registration_v2_page.dart
|
||||||
|
8.3% ( 18/ 217) - lib/features/blog_post/presentation/pages/blog_post_screen.dart
|
||||||
|
11.1% ( 1/ 9) - lib/features/registration_v2/presentation/widgets/step_progress.dart
|
||||||
|
11.8% ( 14/ 119) - lib/features/auth/sdk/social_auth_facade.dart
|
||||||
|
13.0% ( 3/ 23) - lib/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart
|
||||||
|
14.5% ( 11/ 76) - lib/core/app_router.dart
|
||||||
|
19.3% ( 11/ 57) - lib/features/create_recipe/presentation/widgets/image_widget.dart
|
||||||
|
19.6% ( 10/ 51) - lib/features/create_recipe/entities/recipe_create_entity.dart
|
||||||
|
19.7% ( 12/ 61) - lib/features/create_recipe/presentation/widgets/step_image_widget.dart
|
||||||
|
20.0% ( 6/ 30) - lib/core/image_compressing/image_compress_utility.dart
|
||||||
|
20.0% ( 57/ 285) - lib/features/complaining/presentation/complaining_bottom_sheet.dart
|
||||||
|
21.9% ( 7/ 32) - lib/core/routing/content_routes.dart
|
||||||
|
25.6% ( 46/ 180) - lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart
|
||||||
|
26.8% ( 11/ 41) - lib/features/create_recipe/reporitories/recipe_repository.dart
|
||||||
|
27.4% ( 96/ 351) - lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
|
||||||
|
28.0% ( 28/ 100) - lib/core/utils/image_constant.dart
|
||||||
|
28.4% ( 23/ 81) - lib/core/websockets/ws_manager.dart
|
||||||
|
29.0% ( 157/ 541) - lib/features/profile/presentation/pages/profile_screen.dart
|
||||||
|
29.3% ( 137/ 468) - lib/features/notifications/models/notification_model.dart
|
||||||
|
33.1% ( 47/ 142) - lib/features/notifications/presentation/notifications_cubit/notifications_cubit.dart
|
||||||
|
33.3% ( 9/ 27) - lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart
|
||||||
|
35.2% ( 58/ 165) - lib/features/registration/presentation/pages/registration_screen.dart
|
||||||
|
35.6% ( 36/ 101) - lib/features/chat/presentation/pages/empty_chat_screen.dart
|
||||||
|
36.0% ( 9/ 25) - lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart
|
||||||
|
36.7% ( 11/ 30) - lib/core/routing/main_navigation_routes.dart
|
||||||
|
37.5% ( 3/ 8) - lib/core/permission_handler/permission_handler.dart
|
||||||
|
38.6% ( 22/ 57) - lib/features/registration/presentation/pages/date_of_birth_screen.dart
|
||||||
|
38.9% ( 35/ 90) - lib/core/routing/other_routes.dart
|
||||||
|
39.0% ( 30/ 77) - lib/features/create_diet/presentation/widgets/diet_image_picker_widget.dart
|
||||||
|
39.3% ( 119/ 303) - lib/features/settings/presentation/pages/settings_screen.dart
|
||||||
|
39.3% ( 22/ 56) - lib/features/consultant/become_consultant/presentation/pages/photo_or_scans_screen.dart
|
||||||
|
40.0% ( 4/ 10) - lib/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart
|
||||||
|
40.0% ( 4/ 10) - lib/features/registration/services/registration_analytics_service.dart
|
||||||
|
41.5% ( 95/ 229) - lib/features/create_diet/presentation/cubit/create_diet_cubit.dart
|
||||||
|
41.9% ( 31/ 74) - lib/features/create_plan/presentation/widgets/plan_image_picker_widget.dart
|
||||||
|
42.7% ( 67/ 157) - lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart
|
||||||
@@ -1 +1,94 @@
|
|||||||
{"agcgw":{"url":"connect-drru.hispace.dbankcloud.ru","backurl":"connect-drru.hispace.dbankcloud.cn","websocketurl":"connect-ws-drru.hispace.dbankcloud.ru","websocketbackurl":"connect-ws-drru.hispace.dbankcloud.cn"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"20007000036693451","product_id":"461323198429446894","client_id":"1595824543740483264","client_secret":"2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE","project_id":"461323198429446894","app_id":"113238569","api_key":"DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==","package_name":"com.eatvioapp.app"},"oauth_client":{"client_id":"113238569","client_type":1},"app_info":{"app_id":"113238569","package_name":"com.eatvioapp.app"},"service":{"analytics":{"collector_url":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drru.cloud.huawei.ru"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"RU","configuration_version":"3.0","appInfos":[{"package_name":"com.eatvioapp.app","client":{"app_id":"113238569"},"app_info":{"package_name":"com.eatvioapp.app","app_id":"113238569"},"oauth_client":{"client_type":1,"client_id":"113238569"}}]}
|
{
|
||||||
|
"agcgw" : {
|
||||||
|
"url" : "connect-drru.hispace.dbankcloud.ru",
|
||||||
|
"backurl" : "connect-drru.hispace.dbankcloud.cn",
|
||||||
|
"websocketurl" : "connect-ws-drru.hispace.dbankcloud.ru",
|
||||||
|
"websocketbackurl" : "connect-ws-drru.hispace.dbankcloud.cn"
|
||||||
|
},
|
||||||
|
"agcgw_all" : {
|
||||||
|
"SG" : "connect-dra.dbankcloud.cn",
|
||||||
|
"SG_back" : "connect-dra.hispace.hicloud.com",
|
||||||
|
"CN" : "connect-drcn.dbankcloud.cn",
|
||||||
|
"CN_back" : "connect-drcn.hispace.hicloud.com",
|
||||||
|
"RU" : "connect-drru.hispace.dbankcloud.ru",
|
||||||
|
"RU_back" : "connect-drru.hispace.dbankcloud.cn",
|
||||||
|
"DE" : "connect-dre.dbankcloud.cn",
|
||||||
|
"DE_back" : "connect-dre.hispace.hicloud.com"
|
||||||
|
},
|
||||||
|
"websocketgw_all" : {
|
||||||
|
"SG" : "connect-ws-dra.hispace.dbankcloud.cn",
|
||||||
|
"SG_back" : "connect-ws-dra.hispace.dbankcloud.com",
|
||||||
|
"CN" : "connect-ws-drcn.hispace.dbankcloud.cn",
|
||||||
|
"CN_back" : "connect-ws-drcn.hispace.dbankcloud.com",
|
||||||
|
"RU" : "connect-ws-drru.hispace.dbankcloud.ru",
|
||||||
|
"RU_back" : "connect-ws-drru.hispace.dbankcloud.cn",
|
||||||
|
"DE" : "connect-ws-dre.hispace.dbankcloud.cn",
|
||||||
|
"DE_back" : "connect-ws-dre.hispace.dbankcloud.com"
|
||||||
|
},
|
||||||
|
"client" : {
|
||||||
|
"cp_id" : "20007000036693451",
|
||||||
|
"product_id" : "461323198429446894",
|
||||||
|
"client_id" : "1595824543740483264",
|
||||||
|
"client_secret" : "2A1727B0D31AA9BE1F5D9E8A5CDA1A67B9EF084AC9FB68D6C633A3CCB08ECDEE",
|
||||||
|
"project_id" : "461323198429446894",
|
||||||
|
"app_id" : "113238569",
|
||||||
|
"api_key" : "DQEDAJNRITp9M3uc1rPHyT8PzLc7bnVcKJAeavZZk0TQNMx9ifvBebr+JOmZe9tEvjnLwv7c1hAHXmPGR/gWG86iABDrCVotq+NGnw==",
|
||||||
|
"package_name" : "com.eatvioapp.app"
|
||||||
|
},
|
||||||
|
"oauth_client" : {
|
||||||
|
"client_id" : "113238569",
|
||||||
|
"client_type" : 1
|
||||||
|
},
|
||||||
|
"app_info" : {
|
||||||
|
"app_id" : "113238569",
|
||||||
|
"package_name" : "com.eatvioapp.app"
|
||||||
|
},
|
||||||
|
"service" : {
|
||||||
|
"analytics" : {
|
||||||
|
"collector_url" : "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||||
|
"collector_url_cn" : "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
|
||||||
|
"collector_url_de" : "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
|
||||||
|
"collector_url_ru" : "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
|
||||||
|
"collector_url_sg" : "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
|
||||||
|
"resource_id" : "p1",
|
||||||
|
"channel_id" : ""
|
||||||
|
},
|
||||||
|
"ml" : {
|
||||||
|
"mlservice_url" : "ml-api-drru.ai.dbankcloud.ru,ml-api-drru.ai.dbankcloud.cn"
|
||||||
|
},
|
||||||
|
"cloudstorage" : {
|
||||||
|
"storage_url" : "https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||||
|
"storage_url_ru" : "https://agc-storage-drru.cloud.huawei.ru",
|
||||||
|
"storage_url_sg" : "https://ops-dra.agcstorage.link",
|
||||||
|
"storage_url_de" : "https://ops-dre.agcstorage.link",
|
||||||
|
"storage_url_cn" : "https://agc-storage-drcn.platform.dbankcloud.cn",
|
||||||
|
"storage_url_ru_back" : "https://agc-storage-drru.cloud.huawei.ru",
|
||||||
|
"storage_url_sg_back" : "https://agc-storage-dra.cloud.huawei.asia",
|
||||||
|
"storage_url_de_back" : "https://agc-storage-dre.cloud.huawei.eu",
|
||||||
|
"storage_url_cn_back" : "https://agc-storage-drcn.cloud.huawei.com.cn"
|
||||||
|
},
|
||||||
|
"search" : {
|
||||||
|
"url" : "https://search-drru.cloud.huawei.ru"
|
||||||
|
},
|
||||||
|
"edukit" : {
|
||||||
|
"edu_url" : "edukit.cloud.huawei.com.cn",
|
||||||
|
"dh_url" : "edukit.cloud.huawei.com.cn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"region" : "RU",
|
||||||
|
"configuration_version" : "3.0",
|
||||||
|
"appInfos" : [ {
|
||||||
|
"package_name" : "com.eatvioapp.app",
|
||||||
|
"client" : {
|
||||||
|
"app_id" : "113238569"
|
||||||
|
},
|
||||||
|
"app_info" : {
|
||||||
|
"package_name" : "com.eatvioapp.app",
|
||||||
|
"app_id" : "113238569"
|
||||||
|
},
|
||||||
|
"oauth_client" : {
|
||||||
|
"client_type" : 1,
|
||||||
|
"client_id" : "113238569"
|
||||||
|
}
|
||||||
|
} ]
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ plugins {
|
|||||||
id 'com.google.gms.google-services'
|
id 'com.google.gms.google-services'
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
id "kotlin-android"
|
id "kotlin-android"
|
||||||
|
id "kotlin-parcelize"
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
id 'com.huawei.agconnect'
|
id 'com.huawei.agconnect'
|
||||||
}
|
}
|
||||||
@@ -68,10 +69,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
|
implementation 'com.huawei.agconnect:agconnect-core:1.9.5.300'
|
||||||
implementation 'com.huawei.hms:push:6.12.0.300'
|
implementation 'com.huawei.hms:push:6.13.0.301'
|
||||||
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
|
// Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
|
||||||
implementation 'com.huawei.hms:hianalytics:6.12.0.300'
|
implementation 'com.huawei.hms:hianalytics:6.12.0.301'
|
||||||
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
|
// BouncyCastle provider required by Huawei secure utils used transitively by HMS
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
|
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="push_kit_auto_init_enabled"
|
android:name="push_kit_auto_init_enabled"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.huawei.hms.client.appid"
|
||||||
|
android:value="appid=113238569" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.huawei.hms.client.cpid"
|
||||||
|
android:value="cpid=20007000036693451" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -49,32 +55,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" />
|
||||||
|
|
||||||
<data android:pathPattern="/ru/recipe/.*" />
|
<!-- Development/Subdomains -->
|
||||||
<data android:pathPattern="/en/recipe/.*" />
|
<data android:host="*.eatvio.ru" />
|
||||||
|
<data android:host="*.eatvio.com" />
|
||||||
|
|
||||||
<data android:pathPattern="/ru/blog_post/.*" />
|
<!-- Simplify patterns to allow navigation within the app for all matching hosts -->
|
||||||
<data android:pathPattern="/en/blog_post/.*" />
|
<data android:pathPattern="/.*" />
|
||||||
|
|
||||||
<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.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -47,17 +48,21 @@ class MainActivity: FlutterActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appId.isNullOrBlank()) {
|
if (appId.isNullOrBlank()) {
|
||||||
|
android.util.Log.e("HMS", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.")
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
|
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
android.util.Log.i("HMS", "Getting token for appId: $appId")
|
||||||
val token = HmsInstanceId.getInstance(applicationContext)
|
val token = HmsInstanceId.getInstance(applicationContext)
|
||||||
.getToken(appId, "HCM")
|
.getToken(appId, "HCM")
|
||||||
|
android.util.Log.i("HMS", "Token retrieved successfully")
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
result.success(token)
|
result.success(token)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("HMS", "Error getting HMS token: ${e.message}", e)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
result.error("HMS_TOKEN_ERROR", e.message, null)
|
result.error("HMS_TOKEN_ERROR", e.message, null)
|
||||||
}
|
}
|
||||||
@@ -67,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
assets/fonts/Mulish-Black.ttf
Normal file
BIN
assets/fonts/Mulish-Black.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-BlackItalic.ttf
Normal file
BIN
assets/fonts/Mulish-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Bold.ttf
Normal file
BIN
assets/fonts/Mulish-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-BoldItalic.ttf
Normal file
BIN
assets/fonts/Mulish-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraBold.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraBoldItalic.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraLight.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-ExtraLightItalic.ttf
Normal file
BIN
assets/fonts/Mulish-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Italic.ttf
Normal file
BIN
assets/fonts/Mulish-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Light.ttf
Normal file
BIN
assets/fonts/Mulish-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-LightItalic.ttf
Normal file
BIN
assets/fonts/Mulish-LightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Medium.ttf
Normal file
BIN
assets/fonts/Mulish-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-MediumItalic.ttf
Normal file
BIN
assets/fonts/Mulish-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-Regular.ttf
Normal file
BIN
assets/fonts/Mulish-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-SemiBold.ttf
Normal file
BIN
assets/fonts/Mulish-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Mulish-SemiBoldItalic.ttf
Normal file
BIN
assets/fonts/Mulish-SemiBoldItalic.ttf
Normal file
Binary file not shown.
3
assets/icons/crossed_eye.svg
Normal file
3
assets/icons/crossed_eye.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -388,7 +388,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 83;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -539,7 +539,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 83;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 83;
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>83</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|||||||
@@ -48,8 +48,33 @@ platform :ios do
|
|||||||
UI.message "Configuring Flutter for release mode..."
|
UI.message "Configuring Flutter for release mode..."
|
||||||
sh("cd .. && flutter build ios --config-only --release")
|
sh("cd .. && flutter build ios --config-only --release")
|
||||||
|
|
||||||
|
# Read build number from Flutter Generated.xcconfig
|
||||||
|
generated_xcconfig_path = File.join("..", "Flutter", "Generated.xcconfig")
|
||||||
|
flutter_build_number = nil
|
||||||
|
|
||||||
|
if File.exist?(generated_xcconfig_path)
|
||||||
|
File.foreach(generated_xcconfig_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_BUILD_NUMBER=(.*)/)
|
||||||
|
if matches
|
||||||
|
flutter_build_number = matches[1].strip
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if flutter_build_number.nil?
|
||||||
|
UI.user_error!("Could not find FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}")
|
||||||
|
end
|
||||||
|
|
||||||
|
UI.message "Flutter build number: #{flutter_build_number}"
|
||||||
|
|
||||||
match(type: "appstore", readonly: true)
|
match(type: "appstore", readonly: true)
|
||||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
|
||||||
|
# Set build number explicitly instead of incrementing
|
||||||
|
increment_build_number(
|
||||||
|
xcodeproj: "Runner.xcodeproj",
|
||||||
|
build_number: flutter_build_number
|
||||||
|
)
|
||||||
|
|
||||||
build_app(
|
build_app(
|
||||||
workspace: "Runner.xcworkspace",
|
workspace: "Runner.xcworkspace",
|
||||||
@@ -74,7 +99,7 @@ platform :ios do
|
|||||||
skip_submission: true,
|
skip_submission: true,
|
||||||
distribute_external: false,
|
distribute_external: false,
|
||||||
notify_external_testers: false,
|
notify_external_testers: false,
|
||||||
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
|
changelog: "TestFlight build #{flutter_build_number}",
|
||||||
verbose: true
|
verbose: true
|
||||||
)
|
)
|
||||||
rescue => ex
|
rescue => ex
|
||||||
|
|||||||
@@ -33,15 +33,16 @@ class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Список глобальных BlocProvider'ов
|
/// Список глобальных BlocProvider'ов
|
||||||
|
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
|
||||||
List<BlocProvider> get globalBlocProviders => [
|
List<BlocProvider> get globalBlocProviders => [
|
||||||
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
|
BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
|
||||||
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
|
BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
|
||||||
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
|
BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
|
||||||
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
|
BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
|
||||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
|
||||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
|
||||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||||
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
|
BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Делегаты локализации
|
/// Делегаты локализации
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
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';
|
||||||
@@ -14,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();
|
||||||
@@ -37,9 +41,19 @@ class AppInitializer {
|
|||||||
|
|
||||||
/// Инициализация Firebase
|
/// Инициализация Firebase
|
||||||
static Future<void> _initializeFirebase() async {
|
static Future<void> _initializeFirebase() async {
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('Firebase initialization timeout');
|
||||||
|
},
|
||||||
|
);
|
||||||
if (sl<AppConfig>().buildType == AppBuildType.prod) {
|
if (sl<AppConfig>().buildType == AppBuildType.prod) {
|
||||||
await FirebaseMessaging.instance.requestPermission();
|
try {
|
||||||
|
await FirebaseMessaging.instance.requestPermission().timeout(const Duration(seconds: 5));
|
||||||
|
} catch (e) {
|
||||||
|
// Разрешение не критично, продолжаем работу
|
||||||
|
log('Failed to request Firebase messaging permission: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +81,11 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
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';
|
||||||
@@ -5,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';
|
||||||
@@ -13,6 +16,9 @@ import 'package:go_router/go_router.dart';
|
|||||||
class AppRouter {
|
class AppRouter {
|
||||||
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
static const List<String> deepLinkHosts = ['eatvio.ru', 'eatvio.com', 'dev2.eatvio.com'];
|
||||||
|
static const List<String> metaSegments = ['ru', 'en', 'api', 'v2'];
|
||||||
|
|
||||||
static GoRouter appRouter = GoRouter(
|
static GoRouter appRouter = GoRouter(
|
||||||
initialLocation: RouteConstants.updateScreen,
|
initialLocation: RouteConstants.updateScreen,
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
@@ -26,52 +32,212 @@ class AppRouter {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Обработка редиректов для deep links
|
static String? _resolveRecipeDeepLink(List<String> effectiveSegments) {
|
||||||
|
if (effectiveSegments.length != 2) return null;
|
||||||
|
final contentIdentifier = effectiveSegments[1];
|
||||||
|
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 segments = uri.pathSegments;
|
||||||
|
|
||||||
|
final isDeepLink = deepLinkHosts.any((allowedHost) => host == allowedHost || host.endsWith('.$allowedHost'));
|
||||||
|
|
||||||
|
if (!isDeepLink) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
developer.log('Deep link detected: host=$host, path=${uri.path}, segments=$segments', name: 'AppRouter');
|
||||||
|
|
||||||
|
if (segments.isEmpty) {
|
||||||
|
developer.log('Empty segments in deep link, redirecting to feed', name: 'AppRouter');
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
final effectiveSegments = segments.where((segment) => !metaSegments.contains(segment)).toList();
|
||||||
|
if (effectiveSegments.isEmpty) {
|
||||||
|
developer.log('No effective segments after language check, redirecting to feed', name: 'AppRouter');
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
final type = effectiveSegments[0];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'recipe':
|
||||||
|
final target = _resolveRecipeDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'blog_post':
|
||||||
|
final target = _resolveBlogPostDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'plan':
|
||||||
|
final target = _resolvePlanDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'diet':
|
||||||
|
final target = _resolveDietDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'chat':
|
||||||
|
final target = _resolveChatDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'profile':
|
||||||
|
final target = _resolveProfileDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'badge':
|
||||||
|
final target = _resolveBadgeDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'diary':
|
||||||
|
final target = _resolveDiaryDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'consultant_diary_comment':
|
||||||
|
final target = _resolveConsultantDiaryCommentDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
case 'consultations':
|
||||||
|
final target = _resolveConsultationsDeepLink(effectiveSegments);
|
||||||
|
if (target != null) return target;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||||
final segments = state.uri.pathSegments;
|
return _resolveDeepLinkUri(state.uri);
|
||||||
if (segments.length == 4) {
|
}
|
||||||
final type = segments[1];
|
|
||||||
switch (type) {
|
|
||||||
case 'diary':
|
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return '/diary-screen';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (segments.length == 3) {
|
|
||||||
final type = segments[1];
|
|
||||||
final contentIdentifier = segments[2];
|
|
||||||
|
|
||||||
switch (type) {
|
static void navigateByDeepLinkUrl(String url) {
|
||||||
case 'recipe':
|
if (url.isEmpty) {
|
||||||
FlutterNativeSplash.remove();
|
developer.log('navigateByDeepLinkUrl: empty url, redirecting to feed', name: 'AppRouter');
|
||||||
return '/about-recipes-screen?slug=$contentIdentifier';
|
appRouter.go(RouteConstants.feedScreen);
|
||||||
case 'blog_post':
|
return;
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return '/blog-post-screen?id=$contentIdentifier';
|
|
||||||
case 'plan':
|
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return '/about-plan-screen?slug=$contentIdentifier';
|
|
||||||
case 'diet':
|
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return '/about-diet-screen?slug=$contentIdentifier';
|
|
||||||
case 'chat':
|
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return '/chat-select-screen';
|
|
||||||
case 'profile':
|
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return '/profile-screen?slug=$contentIdentifier';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (segments.length == 2) {
|
|
||||||
final type = segments[1];
|
|
||||||
switch (type) {
|
|
||||||
case 'chat':
|
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return '/chat-select-screen';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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}) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +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) {
|
|
||||||
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) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,4 +69,9 @@ class AppConstants {
|
|||||||
static const String usersEndpoint = '/users';
|
static const String usersEndpoint = '/users';
|
||||||
static const String postsEndpoint = '/posts';
|
static const String postsEndpoint = '/posts';
|
||||||
static const String commentsEndpoint = '/comments';
|
static const String commentsEndpoint = '/comments';
|
||||||
|
|
||||||
|
// Delay of pending requests
|
||||||
|
/// 30 секунд стоит потому что запрос может идти 12-15 секунд
|
||||||
|
/// Особенно если производится отладка на сервере разработчика
|
||||||
|
static const Duration pendingDelay = Duration(seconds: 30);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class StateUtils {
|
|||||||
if (isError(state)) {
|
if (isError(state)) {
|
||||||
// Попытка извлечь сообщение об ошибке из состояния
|
// Попытка извлечь сообщение об ошибке из состояния
|
||||||
final stateString = state.toString();
|
final stateString = state.toString();
|
||||||
final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString);
|
final errorMatch = RegExp(r'error[:\s]+([^,}]+)', caseSensitive: false).firstMatch(stateString);
|
||||||
return errorMatch?.group(1)?.trim();
|
return errorMatch?.group(1)?.trim();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_
|
|||||||
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
|
||||||
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
||||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_cubit.dart';
|
||||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
|
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
|
||||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||||
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
|
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
|
||||||
@@ -76,10 +76,10 @@ class CubitRegistrations {
|
|||||||
/// Регистрация Cubit'ов как фабрик
|
/// Регистрация Cubit'ов как фабрик
|
||||||
static void _registerFactoryCubits() {
|
static void _registerFactoryCubits() {
|
||||||
// Auth related cubits
|
// Auth related cubits
|
||||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
|
||||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
||||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
|
||||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
||||||
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
|
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
|
||||||
// Feed related cubits
|
// Feed related cubits
|
||||||
@@ -114,7 +114,7 @@ class CubitRegistrations {
|
|||||||
// User content cubits
|
// User content cubits
|
||||||
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
|
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
|
||||||
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
|
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
|
||||||
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
|
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl(), recipeRepository: sl()));
|
||||||
|
|
||||||
// Other feature cubits
|
// Other feature cubits
|
||||||
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
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/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';
|
||||||
|
|
||||||
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
||||||
class ExternalRegistrations {
|
class ExternalRegistrations {
|
||||||
@@ -12,6 +17,7 @@ class ExternalRegistrations {
|
|||||||
|
|
||||||
/// Регистрация всех внешних зависимостей
|
/// Регистрация всех внешних зависимостей
|
||||||
static void registerAll() {
|
static void registerAll() {
|
||||||
|
sl.registerLazySingleton<ErrorReporter>(SentryErrorReporter.new);
|
||||||
_registerDio();
|
_registerDio();
|
||||||
_registerPrefs();
|
_registerPrefs();
|
||||||
}
|
}
|
||||||
@@ -19,14 +25,23 @@ class ExternalRegistrations {
|
|||||||
/// Регистрация Dio HTTP клиента
|
/// Регистрация Dio HTTP клиента
|
||||||
static void _registerDio() {
|
static void _registerDio() {
|
||||||
sl.registerLazySingleton(
|
sl.registerLazySingleton(
|
||||||
() =>
|
() => Dio(
|
||||||
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
BaseOptions(
|
||||||
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
baseUrl: sl<AppConfig>().baseUrl,
|
||||||
|
headers: {'accept': 'application/json', 'content-type': 'application/json'},
|
||||||
|
connectTimeout: AppConstants.pendingDelay,
|
||||||
|
receiveTimeout: AppConstants.pendingDelay,
|
||||||
|
sendTimeout: AppConstants.pendingDelay,
|
||||||
|
),
|
||||||
|
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
|
||||||
|
..addSentry(
|
||||||
|
failedRequestStatusCodes: const [SentryStatusCode.defaultRange()],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Регистрация Prefs
|
/// Регистрация Prefs
|
||||||
static void _registerPrefs() {
|
static void _registerPrefs() {
|
||||||
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance()));
|
sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
|
|||||||
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||||
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/glossary/repositories/glossary_repository.dart';
|
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
|
||||||
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
|
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
|
||||||
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
||||||
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
|
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
|
||||||
@@ -55,6 +56,7 @@ class RepositoryRegistrations {
|
|||||||
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
|
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
|
||||||
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
|
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
|
||||||
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
|
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
|
||||||
|
sl.registerLazySingleton<BarcodeRepository>(() => BarcodeRepository(barcodeService: sl(), dio: sl()));
|
||||||
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
|
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
|
||||||
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
|
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
|
||||||
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
|
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import 'package:eatvio_app/features/diary/services/diary_service.dart';
|
|||||||
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
import 'package:eatvio_app/features/diets/services/diet_service.dart';
|
||||||
import 'package:eatvio_app/features/feed/services/feed_service.dart';
|
import 'package:eatvio_app/features/feed/services/feed_service.dart';
|
||||||
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
|
import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
|
||||||
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
|
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
|
||||||
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
|
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
|
||||||
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
||||||
@@ -56,6 +57,7 @@ class ServiceRegistrations {
|
|||||||
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
|
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
|
||||||
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
|
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
|
||||||
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
|
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
|
||||||
|
sl.registerLazySingleton<BarcodeService>(() => BarcodeService(sl()));
|
||||||
sl.registerLazySingleton<DietService>(() => DietService(sl()));
|
sl.registerLazySingleton<DietService>(() => DietService(sl()));
|
||||||
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
|
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
|
||||||
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
|
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
|
||||||
@@ -70,7 +72,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()));
|
||||||
|
|||||||
80
lib/core/error_reporting/error_report_policy.dart
Normal file
80
lib/core/error_reporting/error_report_policy.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib/core/error_reporting/error_reporter.dart
Normal file
9
lib/core/error_reporting/error_reporter.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
14
lib/core/error_reporting/no_op_error_reporter.dart
Normal file
14
lib/core/error_reporting/no_op_error_reporter.dart
Normal 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 {}
|
||||||
|
}
|
||||||
17
lib/core/error_reporting/report_from.dart
Normal file
17
lib/core/error_reporting/report_from.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
33
lib/core/error_reporting/sentry_error_reporter.dart
Normal file
33
lib/core/error_reporting/sentry_error_reporter.dart
Normal 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())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,47 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
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: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;
|
||||||
|
|
||||||
if (await _isHuaweiDevice()) {
|
if (await _isHuaweiDevice()) {
|
||||||
token = await _getHuaweiPushTokenNative();
|
token = await _getHuaweiPushTokenNative().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => null,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
|
if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
|
||||||
await _firebaseMessaging.requestPermission();
|
try {
|
||||||
|
await _firebaseMessaging.requestPermission().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Разрешение не критично, продолжаем
|
||||||
|
log('Failed to request Firebase messaging permission: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
token = await _firebaseMessaging.getToken();
|
token = await _firebaseMessaging.getToken().timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
@@ -35,13 +52,20 @@ class FirebaseApi {
|
|||||||
await sl<Dio>().post(
|
await sl<Dio>().post(
|
||||||
'/api/anonymous_token',
|
'/api/anonymous_token',
|
||||||
data: {
|
data: {
|
||||||
'platform': Platform.isIOS ? 'ios' : 'android',
|
'platform': Platform.isIOS ? 'ios' : (await _isHuaweiDevice()) ? 'huawei' : 'android',
|
||||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||||
'token': await getToken(),
|
'token': await getToken(),
|
||||||
},
|
},
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('registerAnonymousPushToken timeout');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
await _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||||
|
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +81,16 @@ class FirebaseApi {
|
|||||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||||
'token': await getToken(),
|
'token': await getToken(),
|
||||||
},
|
},
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: 10),
|
||||||
|
onTimeout: () {
|
||||||
|
throw TimeoutException('registerPushToken timeout');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
log(e.toString(), stackTrace: st);
|
log(e.toString(), stackTrace: st);
|
||||||
|
await _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||||
|
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,10 +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 _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 _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:eatvio_app/core/app_initializer.dart';
|
||||||
|
import 'package:eatvio_app/core/app_config.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 {
|
||||||
@@ -27,33 +29,21 @@ abstract class ImageCompressUtility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<File> compressFile(File file) async {
|
static Future<File> compressFile(File file) async {
|
||||||
final image = img.decodeImage(file.readAsBytesSync())!;
|
try {
|
||||||
int width = image.width;
|
final appConfig = sl<AppConfig>();
|
||||||
int height = image.height;
|
if (appConfig.buildType == AppBuildType.tests) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
if (width > height) {
|
final compressedPath = '${p.withoutExtension(file.path)}_compressed.jpg';
|
||||||
if (width > 1080) {
|
|
||||||
height = (height * 1080) ~/ width;
|
final result = await FlutterImageCompress.compressAndGetFile(file.absolute.path, compressedPath, quality: 90, minHeight: 1080, minWidth: 1080);
|
||||||
width = 1080;
|
|
||||||
}
|
if (result == null) {
|
||||||
} else {
|
return file;
|
||||||
if (height > 1080) {
|
|
||||||
width = (width * 1080) ~/ height;
|
|
||||||
height = 1080;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String filePathWithoutExtension = p.withoutExtension(file.path);
|
return File(result.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -14,16 +15,19 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class TokenInterceptor extends Interceptor {
|
class TokenInterceptor extends Interceptor {
|
||||||
@@ -23,6 +25,7 @@ class TokenInterceptor extends Interceptor {
|
|||||||
ErrorInterceptorHandler handler,
|
ErrorInterceptorHandler handler,
|
||||||
) async {
|
) async {
|
||||||
if (err.response?.statusCode == 401) {
|
if (err.response?.statusCode == 401) {
|
||||||
|
await sl<WsManager>().clear();
|
||||||
await TokenStorage.removeTokenInStorage();
|
await TokenStorage.removeTokenInStorage();
|
||||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
|
|||||||
import 'package:eatvio_app/core/di/external_registrations.dart';
|
import 'package:eatvio_app/core/di/external_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
import 'package:eatvio_app/core/di/repository_registrations.dart';
|
||||||
import 'package:eatvio_app/core/di/service_registrations.dart';
|
import 'package:eatvio_app/core/di/service_registrations.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
|
|||||||
// Регистрация внешних зависимостей (Dio, etc.)
|
// Регистрация внешних зависимостей (Dio, etc.)
|
||||||
ExternalRegistrations.registerAll();
|
ExternalRegistrations.registerAll();
|
||||||
|
|
||||||
|
// Ожидание инициализации асинхронных зависимостей
|
||||||
|
await sl.getAsync<Prefs>();
|
||||||
|
|
||||||
// Регистрация сервисов
|
// Регистрация сервисов
|
||||||
ServiceRegistrations.registerAll();
|
ServiceRegistrations.registerAll();
|
||||||
|
|
||||||
@@ -28,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()));
|
||||||
}
|
}
|
||||||
|
|||||||
64
lib/core/notifications/push_navigation_handler.dart
Normal file
64
lib/core/notifications/push_navigation_handler.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }
|
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped }
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'package:eatvio_app/core/firebase_api.dart';
|
|
||||||
import 'package:eatvio_app/core/locator_service.dart';
|
|
||||||
import 'package:eatvio_app/core/routing/route_constants.dart';
|
import 'package:eatvio_app/core/routing/route_constants.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/auth/presentation/pages/auth_screen.dart';
|
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||||
@@ -12,19 +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(),
|
||||||
redirect: (context, state) {
|
|
||||||
sl<FirebaseApi>().registerAnonymousPushToken();
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
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()),
|
||||||
|
|||||||
@@ -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?);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -23,14 +24,16 @@ class MainNavigationRoutes {
|
|||||||
return [
|
return [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) {
|
builder: (context, state, child) {
|
||||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
|
||||||
builder: (BuildContext context, ProfileAvatarState state) {
|
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
|
||||||
if (state is ProfileAvatarStateLoaded) {
|
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
|
||||||
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
|
// Вызываем getMyProfile() только если состояние loading (первый запуск)
|
||||||
}
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
return const SizedBox.shrink();
|
profileAvatarCubit.getMyProfile();
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
@@ -43,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();
|
||||||
@@ -56,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,
|
||||||
@@ -66,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,
|
||||||
@@ -76,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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
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/firebase_api.dart';
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
@@ -6,6 +7,7 @@ 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:eatvio_app/core/routing/route_constants.dart';
|
import 'package:eatvio_app/core/routing/route_constants.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||||
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
|
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
|
||||||
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
|
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
|
||||||
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
||||||
@@ -58,24 +60,33 @@ class OtherRoutes {
|
|||||||
builder: (context, state) => const WelcomeScreen(),
|
builder: (context, state) => const WelcomeScreen(),
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
try {
|
try {
|
||||||
final prefs = sl<Prefs>();
|
// Prefs уже инициализирован в locator_service, но на всякий случай проверяем
|
||||||
final firebaseApi = sl<FirebaseApi>();
|
final prefs = await sl.getAsync<Prefs>();
|
||||||
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
||||||
await firebaseApi.registerPushToken();
|
|
||||||
if (!welcomeScreenWasShowed) {
|
if (!welcomeScreenWasShowed) {
|
||||||
return null;
|
return null;
|
||||||
} else if (await TokenStorage.haveTokenInStorage()) {
|
}
|
||||||
return '/feed-screen';
|
|
||||||
} else {
|
if (await TokenStorage.haveTokenInStorage()) {
|
||||||
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
try {
|
||||||
if (!hasAnonymousTokenBeenSent) {
|
final profileRepository = sl<ProfileRepository>();
|
||||||
await firebaseApi.registerAnonymousPushToken();
|
final profile = await profileRepository.getMyProfile();
|
||||||
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
|
||||||
|
final firebaseApi = sl<FirebaseApi>();
|
||||||
|
await firebaseApi.registerPushToken();
|
||||||
|
|
||||||
|
if (profile.emailVerifiedAt != null || (prefs.getBool(PrefsKeys.isEmailVerificationSkipped) ?? false)) {
|
||||||
|
return RouteConstants.feedScreen;
|
||||||
|
}
|
||||||
|
return RouteConstants.verifyEmailScreen;
|
||||||
|
} catch (e) {
|
||||||
|
return '/auth-screen';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return '/auth-screen';
|
return '/auth-screen';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return '/feed-screen';
|
return '/auth-screen';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -85,7 +96,27 @@ class OtherRoutes {
|
|||||||
builder: (context, state) => const UpdateScreen(),
|
builder: (context, state) => const UpdateScreen(),
|
||||||
redirect: (context, state) async {
|
redirect: (context, state) async {
|
||||||
try {
|
try {
|
||||||
final versions = (await sl<Dio>().get('${sl<AppConfig>().baseUrl}/api/app_versions')).data;
|
// Проверка версии с таймаутом, чтобы не блокировать запуск приложения
|
||||||
|
final versions = await sl<Dio>()
|
||||||
|
.get('${sl<AppConfig>().baseUrl}/api/app_versions')
|
||||||
|
.timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () {
|
||||||
|
// При таймауте пропускаем проверку и идем дальше
|
||||||
|
throw TimeoutException('Version check timeout');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((response) => response.data)
|
||||||
|
.catchError((e) {
|
||||||
|
// При любой ошибке пропускаем проверку версии
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (versions == null) {
|
||||||
|
// Если проверка не удалась, пропускаем экран обновления
|
||||||
|
return RouteConstants.welcomeScreen;
|
||||||
|
}
|
||||||
|
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
String version = packageInfo.version;
|
String version = packageInfo.version;
|
||||||
Version currentVersion = Version.parse(version);
|
Version currentVersion = Version.parse(version);
|
||||||
@@ -96,6 +127,7 @@ class OtherRoutes {
|
|||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// При любой ошибке пропускаем экран обновления
|
||||||
return RouteConstants.welcomeScreen;
|
return RouteConstants.welcomeScreen;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -121,22 +153,32 @@ class OtherRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RecipeCreateScreen.path,
|
path: RecipeCreateScreen.path,
|
||||||
name: RecipeCreateScreen.name,
|
name: RecipeCreateScreen.name,
|
||||||
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
|
builder: (context, state) {
|
||||||
|
if (state.extra is Map<String, dynamic>) {
|
||||||
|
final data = state.extra as Map<String, dynamic>;
|
||||||
|
return RecipeCreateScreen(recipe: data['recipe'] as Recipe?, autoValidate: data['autoValidate'] as bool? ?? false);
|
||||||
|
}
|
||||||
|
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: CreateDietScreen.path,
|
path: CreateDietScreen.path,
|
||||||
name: CreateDietScreen.name,
|
name: CreateDietScreen.name,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final diet = state.extra as Diet?;
|
final data = state.extra as Map<String, dynamic>?;
|
||||||
return CreateDietScreen(diet: diet);
|
final diet = data?['diet'] as Diet?;
|
||||||
|
final isEditing = data?['isEditing'] as bool?;
|
||||||
|
return CreateDietScreen(diet: diet, isEditing: isEditing);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: DietPreviewScreen.path,
|
path: DietPreviewScreen.path,
|
||||||
name: DietPreviewScreen.name,
|
name: DietPreviewScreen.name,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final data = state.extra as Diet;
|
final data = state.extra as Map<String, dynamic>;
|
||||||
return DietPreviewScreen(diet: data);
|
final diet = data['diet'] as Diet;
|
||||||
|
final isImageInBase64 = data['isImageInBase64'] as bool;
|
||||||
|
return DietPreviewScreen(diet: diet, isImageInBase64: isImageInBase64);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
|
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
|
||||||
@@ -159,7 +201,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
|
||||||
@@ -167,6 +209,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?;
|
||||||
@@ -184,7 +230,12 @@ class OtherRoutes {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteConstants.stepperBecomeConsultantScreen,
|
path: RouteConstants.stepperBecomeConsultantScreen,
|
||||||
name: 'stepperBecomeConsultantScreen',
|
name: 'stepperBecomeConsultantScreen',
|
||||||
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String),
|
builder: (context, state) {
|
||||||
|
final data = state.extra as Map<String, dynamic>;
|
||||||
|
final slug = data['slug'] as String?;
|
||||||
|
final isCanceled = data['isCanceled'] as bool?;
|
||||||
|
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()),
|
||||||
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
||||||
@@ -217,6 +268,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?;
|
||||||
@@ -240,14 +296,13 @@ 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(
|
||||||
path: PersonalDietSuggestionScreen.path,
|
path: PersonalDietSuggestionScreen.path,
|
||||||
name: PersonalDietSuggestionScreen.name,
|
name: PersonalDietSuggestionScreen.name,
|
||||||
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
|
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
|
|
||||||
/// Helper class for managing themes and colors.
|
/// Helper class for managing themes and colors.
|
||||||
class ThemeHelper {
|
class ThemeHelper {
|
||||||
@@ -34,7 +34,8 @@ class ThemeHelper {
|
|||||||
|
|
||||||
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
|
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
textTheme: GoogleFonts.mulishTextTheme(),
|
fontFamily: 'Mulish-Local',
|
||||||
|
textTheme: ThemeData.light().textTheme.apply(fontFamily: 'Mulish-Local'),
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
scaffoldBackgroundColor: colorScheme.onPrimary,
|
scaffoldBackgroundColor: colorScheme.onPrimary,
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
abstract class TokenStorage {
|
abstract class TokenStorage {
|
||||||
|
static SharedPreferences? _cachedPrefs;
|
||||||
|
|
||||||
|
static Future<SharedPreferences> _getPrefs() async {
|
||||||
|
_cachedPrefs ??= await SharedPreferences.getInstance();
|
||||||
|
return _cachedPrefs!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the cached SharedPreferences instance.
|
||||||
|
/// This is primarily used for testing to ensure fresh state between tests.
|
||||||
|
@visibleForTesting
|
||||||
|
static void clearCache() {
|
||||||
|
_cachedPrefs = null;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<bool> haveTokenInStorage() async {
|
static Future<bool> haveTokenInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
try {
|
try {
|
||||||
|
final sharedPreferences = await _getPrefs();
|
||||||
if (sharedPreferences.getString('token') == null) {
|
if (sharedPreferences.getString('token') == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -15,25 +30,25 @@ abstract class TokenStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> getTokenFromStorage() async {
|
static Future<String?> getTokenFromStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
return sharedPreferences.getString('token');
|
return sharedPreferences.getString('token');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveTokenInStorage(String token) async {
|
static Future<void> saveTokenInStorage(String token) async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
await sharedPreferences.setString('token', token);
|
await sharedPreferences.setString('token', token);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> removeTokenInStorage() async {
|
static Future<void> removeTokenInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await _getPrefs();
|
||||||
await sharedPreferences.remove('token');
|
await sharedPreferences.remove('token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SocketIdStorage {
|
abstract class SocketIdStorage {
|
||||||
static Future<bool> haveSocketIdInStorage() async {
|
static Future<bool> haveSocketIdInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
|
||||||
try {
|
try {
|
||||||
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
if (sharedPreferences.getString('socketId') == null) {
|
if (sharedPreferences.getString('socketId') == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> getSocketIdFromStorage() async {
|
static Future<String?> getSocketIdFromStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
return sharedPreferences.getString('socketId');
|
return sharedPreferences.getString('socketId');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
await sharedPreferences.setString('socketId', socketId);
|
await sharedPreferences.setString('socketId', socketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> removeSocketIdInStorage() async {
|
static Future<void> removeSocketIdInStorage() async {
|
||||||
final sharedPreferences = await SharedPreferences.getInstance();
|
final sharedPreferences = await TokenStorage._getPrefs();
|
||||||
await sharedPreferences.remove('socketId');
|
await sharedPreferences.remove('socketId');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
lib/core/utils/markdown_utils.dart
Normal file
67
lib/core/utils/markdown_utils.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/// Утилиты для обработки markdown текста
|
||||||
|
class MarkdownUtils {
|
||||||
|
/// Обрабатывает markdown текст:
|
||||||
|
/// - Исправляет неправильный синтаксис markdown ссылок
|
||||||
|
/// - Автоматически преобразует URL в markdown ссылки
|
||||||
|
/// - Преобразует **текст** в <b>текст</b> для надежного отображения жирного текста
|
||||||
|
static String processMarkdown(String text) {
|
||||||
|
// Сначала исправляем неправильный синтаксис markdown ссылок
|
||||||
|
// Например: [http://example.com](url) -> [http://example.com](http://example.com)
|
||||||
|
final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
|
||||||
|
text = text.replaceAllMapped(wrongLinkRegex, (match) {
|
||||||
|
final urlInText = match.group(1)!;
|
||||||
|
if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
|
||||||
|
return match.group(0)!;
|
||||||
|
}
|
||||||
|
final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
|
||||||
|
return '[$urlInText]($fullUrl)';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Находим все существующие markdown ссылки и их позиции
|
||||||
|
final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
|
||||||
|
final existingLinks = <_LinkRange>[];
|
||||||
|
for (final linkMatch in linkRegex.allMatches(text)) {
|
||||||
|
existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регулярное выражение для поиска URL (http://, https://, www.)
|
||||||
|
final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
|
||||||
|
|
||||||
|
// Заменяем URL на markdown ссылки, если они еще не являются ссылками
|
||||||
|
String result = text;
|
||||||
|
int offset = 0;
|
||||||
|
for (final urlMatch in urlRegex.allMatches(text)) {
|
||||||
|
// Проверяем, не находится ли URL внутри существующей markdown ссылки
|
||||||
|
bool isInsideLink = existingLinks.any((link) =>
|
||||||
|
urlMatch.start >= link.start && urlMatch.end <= link.end);
|
||||||
|
if (!isInsideLink) {
|
||||||
|
final url = urlMatch.group(0)!;
|
||||||
|
final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
|
||||||
|
final replacement = '[$url]($fullUrl)';
|
||||||
|
result = result.replaceRange(
|
||||||
|
urlMatch.start + offset,
|
||||||
|
urlMatch.end + offset,
|
||||||
|
replacement,
|
||||||
|
);
|
||||||
|
offset += replacement.length - url.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Гарантируем, что жирный текст будет распознан
|
||||||
|
// Заменяем **текст** на <b>текст</b> вручную для надежности
|
||||||
|
final boldRegex = RegExp(r'\*\*([^*]+)\*\*');
|
||||||
|
result = result.replaceAllMapped(boldRegex, (match) {
|
||||||
|
return '<b>${match.group(1)}</b>';
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Вспомогательный класс для хранения диапазона позиций ссылки
|
||||||
|
class _LinkRange {
|
||||||
|
final int start;
|
||||||
|
final int end;
|
||||||
|
|
||||||
|
_LinkRange(this.start, this.end);
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
extension Capitalization on String {
|
extension Capitalization on String {
|
||||||
String capitalize() {
|
String capitalize() {
|
||||||
|
if (isEmpty) return '';
|
||||||
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
String parseGender() {
|
String parseGender() {
|
||||||
if (this == 'male') {
|
if (this == 'male') {
|
||||||
return 'Мужской';
|
return 'Мужской';
|
||||||
} else if (this == 'female') {
|
} else if (this == 'female') {
|
||||||
return 'Женский';
|
return 'Женский';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ class ValidationUtils {
|
|||||||
return regex.hasMatch(phone);
|
return regex.hasMatch(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Нормализация номера телефона (+79991234455)
|
||||||
|
static String normalizePhone(String phone) {
|
||||||
|
if (phone.isEmpty) return '';
|
||||||
|
final digits = phone.replaceAll(RegExp(r'[^\d]'), '');
|
||||||
|
if (digits.isEmpty) return '';
|
||||||
|
|
||||||
|
String normalized = digits;
|
||||||
|
if (digits.startsWith('8')) {
|
||||||
|
normalized = '7${digits.substring(1)}';
|
||||||
|
} else if (digits.length == 10) {
|
||||||
|
normalized = '7$digits';
|
||||||
|
}
|
||||||
|
return '+$normalized';
|
||||||
|
}
|
||||||
|
|
||||||
/// Валидация URL
|
/// Валидация URL
|
||||||
static bool isValidUrl(String url) {
|
static bool isValidUrl(String url) {
|
||||||
if (url.isEmpty) return false;
|
if (url.isEmpty) return false;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -7,10 +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) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
@@ -8,12 +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: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;
|
||||||
@@ -22,6 +27,10 @@ class WsManager {
|
|||||||
PrivateChannel? _privateMessagesChannel;
|
PrivateChannel? _privateMessagesChannel;
|
||||||
PrivateChannel? _privateDirectChatChannel;
|
PrivateChannel? _privateDirectChatChannel;
|
||||||
|
|
||||||
|
StreamSubscription? _lifecycleSubscription;
|
||||||
|
StreamSubscription? _eventSubscription;
|
||||||
|
StreamSubscription? _connectionEstablishedSubscription;
|
||||||
|
|
||||||
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
|
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
|
||||||
|
|
||||||
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
|
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
|
||||||
@@ -33,7 +42,7 @@ class WsManager {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
try {
|
try {
|
||||||
await TokenStorage.haveTokenInStorage();
|
if (!await TokenStorage.haveTokenInStorage()) return;
|
||||||
final wsUrl = await wsService.getWebsocket();
|
final wsUrl = await wsService.getWebsocket();
|
||||||
|
|
||||||
final options = PusherChannelsOptions.fromHost(
|
final options = PusherChannelsOptions.fromHost(
|
||||||
@@ -71,7 +80,7 @@ class WsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client?.eventStream.listen((event) async {
|
_connectionEstablishedSubscription = client?.eventStream.listen((event) async {
|
||||||
if (event.name == 'pusher:connection_established') {
|
if (event.name == 'pusher:connection_established') {
|
||||||
final data = event.data;
|
final data = event.data;
|
||||||
final socketId = extractSocketId(data);
|
final socketId = extractSocketId(data);
|
||||||
@@ -87,26 +96,46 @@ class WsManager {
|
|||||||
await _initPrivateNotificationsChannel();
|
await _initPrivateNotificationsChannel();
|
||||||
await _initPrivateMessagesChannel();
|
await _initPrivateMessagesChannel();
|
||||||
|
|
||||||
client?.lifecycleStream.listen((data) {
|
_lifecycleSubscription = client?.lifecycleStream.listen((data) {
|
||||||
if (data == PusherChannelsClientLifeCycleState.establishedConnection) {
|
if (data == PusherChannelsClientLifeCycleState.establishedConnection) {
|
||||||
_unsubscribe();
|
_unsubscribe();
|
||||||
_subscribe();
|
_subscribe();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client?.eventStream.listen((event) {
|
_eventSubscription = client?.eventStream.listen((event) {
|
||||||
EasyDebounce.debounce(
|
log('WsManager received event: ${event.name}');
|
||||||
'searchChatRoom',
|
// Только если это не системные сообщения Pusher, обновляем список чатов
|
||||||
const Duration(milliseconds: 300),
|
if (!event.name.startsWith('pusher:')) {
|
||||||
() => sl<ChatsCubit>().getChatRooms(),
|
EasyDebounce.debounce(
|
||||||
);
|
'searchChatRoom',
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
() => sl<ChatsCubit>().getChatRooms(),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
_subscribe();
|
_subscribe();
|
||||||
|
await _connectCubitsToStreams();
|
||||||
} catch (e, st) {
|
} catch (e, 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(
|
||||||
@@ -166,6 +195,12 @@ class WsManager {
|
|||||||
|
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
await client?.disconnect();
|
await client?.disconnect();
|
||||||
|
await _lifecycleSubscription?.cancel();
|
||||||
|
await _eventSubscription?.cancel();
|
||||||
|
await _connectionEstablishedSubscription?.cancel();
|
||||||
|
_lifecycleSubscription = null;
|
||||||
|
_eventSubscription = null;
|
||||||
|
_connectionEstablishedSubscription = null;
|
||||||
client = null;
|
client = null;
|
||||||
userId = null;
|
userId = null;
|
||||||
_privateDirectChatChannel?.unsubscribe();
|
_privateDirectChatChannel?.unsubscribe();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class CustomOutlinedButton extends BaseButton {
|
|||||||
margin: margin,
|
margin: margin,
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
style: buttonStyle!,
|
style: buttonStyle ?? OutlinedButton.styleFrom(),
|
||||||
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -59,7 +59,7 @@ class CustomOutlinedButton extends BaseButton {
|
|||||||
margin: margin,
|
margin: margin,
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
style: buttonStyle!,
|
style: buttonStyle ?? OutlinedButton.styleFrom(),
|
||||||
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class FeedIconButton extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (svgIcon != null) svgIcon!,
|
?svgIcon,
|
||||||
if (icon != null)
|
if (icon != null)
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -12,15 +12,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
|
||||||
class InfoRow extends StatelessWidget {
|
class InfoRow extends StatelessWidget {
|
||||||
const InfoRow({
|
const InfoRow({required this.type, super.key, this.diet, this.itemsBackgroundColor, this.disableDescriptionOfTypes = false, this.plan, this.recipe});
|
||||||
required this.type,
|
|
||||||
super.key,
|
|
||||||
this.diet,
|
|
||||||
this.itemsBackgroundColor,
|
|
||||||
this.disableDescriptionOfTypes = false,
|
|
||||||
this.plan,
|
|
||||||
this.recipe,
|
|
||||||
});
|
|
||||||
final ProductType type;
|
final ProductType type;
|
||||||
final Color? itemsBackgroundColor;
|
final Color? itemsBackgroundColor;
|
||||||
final Diet? diet;
|
final Diet? diet;
|
||||||
@@ -50,44 +42,13 @@ class InfoRow extends StatelessWidget {
|
|||||||
InfoBouble(
|
InfoBouble(
|
||||||
backgroundColor: itemsBackgroundColor,
|
backgroundColor: itemsBackgroundColor,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset('assets/icons/time2.svg', width: 13.3, height: 13.3),
|
||||||
'assets/icons/time2.svg',
|
|
||||||
width: 13.3,
|
|
||||||
height: 13.3,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(switch (type) {
|
||||||
switch (type) {
|
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
||||||
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
||||||
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
ProductType.recipe => timeTitle ?? '',
|
||||||
ProductType.recipe => timeTitle ?? '',
|
}, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||||
},
|
|
||||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
InfoBouble(
|
|
||||||
backgroundColor: itemsBackgroundColor,
|
|
||||||
children: [
|
|
||||||
SvgPicture.asset(
|
|
||||||
switch (type) {
|
|
||||||
ProductType.diet => 'assets/icons/people.svg',
|
|
||||||
ProductType.plan => 'assets/icons/people.svg',
|
|
||||||
ProductType.recipe => 'assets/icons/calories.svg',
|
|
||||||
},
|
|
||||||
width: 13.3,
|
|
||||||
height: 13.3,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
switch (type) {
|
|
||||||
ProductType.diet => '80 человек прошли',
|
|
||||||
ProductType.plan => '80 человек прошли',
|
|
||||||
ProductType.recipe => '${recipe?.calories.toStringAsFixed(0) ?? '0'} ккал',
|
|
||||||
},
|
|
||||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -95,11 +56,17 @@ class InfoRow extends StatelessWidget {
|
|||||||
InfoBouble(
|
InfoBouble(
|
||||||
backgroundColor: itemsBackgroundColor,
|
backgroundColor: itemsBackgroundColor,
|
||||||
children: [
|
children: [
|
||||||
SvgPicture.asset(
|
SvgPicture.asset('assets/icons/calories.svg', width: 13.3, height: 13.3),
|
||||||
'assets/icons/steps.svg',
|
const SizedBox(width: 4),
|
||||||
width: 13.3,
|
Text('${(recipe?.calories ?? 0).toStringAsFixed(0)} ккал', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||||
height: 13.3,
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
if (type == ProductType.recipe)
|
||||||
|
InfoBouble(
|
||||||
|
backgroundColor: itemsBackgroundColor,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset('assets/icons/steps.svg', width: 13.3, height: 13.3),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${recipe?.steps?.length ?? 0} ${getStepForm(recipe?.steps?.length ?? 0)}',
|
'${recipe?.steps?.length ?? 0} ${getStepForm(recipe?.steps?.length ?? 0)}',
|
||||||
@@ -110,13 +77,7 @@ class InfoRow extends StatelessWidget {
|
|||||||
if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true)
|
if (plan?.type.hasAtLeastOneType() == true || diet?.type.hasAtLeastOneType() == true)
|
||||||
InfoBouble(
|
InfoBouble(
|
||||||
backgroundColor: itemsBackgroundColor,
|
backgroundColor: itemsBackgroundColor,
|
||||||
children: [
|
children: [TypeBadges(type: diet?.type, planType: plan?.type, disableDescriptionOfTypes: disableDescriptionOfTypes)],
|
||||||
TypeBadges(
|
|
||||||
type: diet?.type,
|
|
||||||
planType: plan?.type,
|
|
||||||
disableDescriptionOfTypes: disableDescriptionOfTypes,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
124
lib/core/widgets/markdown_html_widget.dart
Normal file
124
lib/core/widgets/markdown_html_widget.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:eatvio_app/core/utils/markdown_utils.dart';
|
||||||
|
|
||||||
|
/// Переиспользуемый виджет для отображения HTML из markdown текста
|
||||||
|
class MarkdownHtmlWidget extends StatelessWidget {
|
||||||
|
/// Markdown текст для отображения
|
||||||
|
final String markdownText;
|
||||||
|
|
||||||
|
/// Ширина изображений (по умолчанию ширина экрана минус 32)
|
||||||
|
final double? imageWidth;
|
||||||
|
|
||||||
|
/// Цвет текста для body и параграфов
|
||||||
|
final Color? textColor;
|
||||||
|
|
||||||
|
/// Размер шрифта (по умолчанию 16)
|
||||||
|
final double? fontSize;
|
||||||
|
|
||||||
|
/// Callback для обработки нажатий на ссылки
|
||||||
|
final OnTap? onLinkTap;
|
||||||
|
|
||||||
|
const MarkdownHtmlWidget({
|
||||||
|
super.key,
|
||||||
|
required this.markdownText,
|
||||||
|
this.imageWidth,
|
||||||
|
this.textColor,
|
||||||
|
this.fontSize,
|
||||||
|
this.onLinkTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Обрабатываем markdown текст
|
||||||
|
final processedText = MarkdownUtils.processMarkdown(markdownText);
|
||||||
|
|
||||||
|
// Конвертируем markdown в HTML
|
||||||
|
final htmlData = md.markdownToHtml(
|
||||||
|
processedText,
|
||||||
|
extensionSet: md.ExtensionSet.gitHubFlavored,
|
||||||
|
);
|
||||||
|
|
||||||
|
final screenWidth = MediaQuery.maybeOf(context)?.size.width ?? 400.0;
|
||||||
|
final defaultImageWidth = imageWidth ?? (screenWidth - 32);
|
||||||
|
final defaultTextColor = textColor ?? Colors.black87;
|
||||||
|
final defaultFontSize = fontSize ?? 16.0;
|
||||||
|
|
||||||
|
return Html(
|
||||||
|
data: htmlData,
|
||||||
|
style: {
|
||||||
|
"body": Style(
|
||||||
|
margin: Margins.zero,
|
||||||
|
padding: HtmlPaddings.zero,
|
||||||
|
fontSize: FontSize(defaultFontSize),
|
||||||
|
lineHeight: const LineHeight(1.5),
|
||||||
|
color: defaultTextColor,
|
||||||
|
),
|
||||||
|
"p": Style(
|
||||||
|
margin: Margins.only(bottom: 12),
|
||||||
|
color: defaultTextColor,
|
||||||
|
),
|
||||||
|
"b, strong": Style(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
"h1, h2, h3, h4, h5, h6": Style(
|
||||||
|
margin: Margins.only(top: 16, bottom: 8),
|
||||||
|
color: Colors.black,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
"h1": Style(fontSize: FontSize(24)),
|
||||||
|
"h2": Style(fontSize: FontSize(20)),
|
||||||
|
"h3": Style(fontSize: FontSize(18)),
|
||||||
|
"ul, ol": Style(
|
||||||
|
margin: Margins.only(bottom: 12),
|
||||||
|
padding: HtmlPaddings.only(left: 16),
|
||||||
|
),
|
||||||
|
"li": Style(
|
||||||
|
margin: Margins.only(bottom: 4),
|
||||||
|
color: defaultTextColor,
|
||||||
|
),
|
||||||
|
"strong, b": Style(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
"em, i": Style(fontStyle: FontStyle.italic),
|
||||||
|
"code": Style(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
"pre": Style(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
padding: HtmlPaddings.all(12),
|
||||||
|
margin: Margins.only(bottom: 12),
|
||||||
|
),
|
||||||
|
"blockquote": Style(
|
||||||
|
padding: HtmlPaddings.only(left: 16),
|
||||||
|
margin: Margins.only(left: 8, bottom: 12),
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
"a": Style(
|
||||||
|
color: Colors.blue,
|
||||||
|
textDecoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
"img": Style(width: Width(defaultImageWidth)),
|
||||||
|
},
|
||||||
|
onLinkTap: onLinkTap ?? ((url, attributes, element) async {
|
||||||
|
if (url != null) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error launching URL: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as OnTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import 'package:eatvio_app/core/app_router.dart';
|
|
||||||
import 'package:eatvio_app/core/theme/custom_text_style.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:eatvio_app/generated/l10n.dart';
|
import 'package:eatvio_app/generated/l10n.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
|
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
|
||||||
@@ -45,7 +43,7 @@ Future<void> showErrorDialog(BuildContext context, {String? title, String? descr
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AppRouter.navigatorKey.currentContext?.pop();
|
Navigator.of(context).pop();
|
||||||
if (needToOpenSettings ?? false) {
|
if (needToOpenSettings ?? false) {
|
||||||
openAppSettings();
|
openAppSettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,33 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
||||||
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false});
|
const SuccessPublishedDietOrPlanBottomSheet({super.key, required this.title, this.isPlan = false, this.isDrafted, this.isUpdated});
|
||||||
final String title;
|
final String title;
|
||||||
final bool isPlan;
|
final bool isPlan;
|
||||||
|
final bool? isDrafted;
|
||||||
|
final bool? isUpdated;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
|
final String text;
|
||||||
|
if (isDrafted == true) {
|
||||||
|
text = 'cохранена в черновиках';
|
||||||
|
} else if (isUpdated == true) {
|
||||||
|
text = 'успешно обновлена';
|
||||||
|
} else if (isPlan == true) {
|
||||||
|
text = 'опубликован';
|
||||||
|
} else {
|
||||||
|
text = 'опубликованна';
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(height: 20),
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
const BottomSheetHandlerBar(),
|
const BottomSheetHandlerBar(),
|
||||||
const SizedBox(
|
const SizedBox(height: 20),
|
||||||
height: 20,
|
Text(title, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
),
|
Text(text, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||||
Text(
|
const SizedBox(height: 20),
|
||||||
title,
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
isPlan ? 'опубликован' : 'опубликованна',
|
|
||||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: CustomOutlinedButton(
|
child: CustomOutlinedButton(
|
||||||
@@ -43,15 +44,10 @@ class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
|||||||
text: s.ok,
|
text: s.ok,
|
||||||
height: 52,
|
height: 52,
|
||||||
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
|
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
|
||||||
buttonStyle: ButtonStyle(
|
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
|
||||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 20),
|
||||||
height: 20,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'oauth_verification_confirm_request.freezed.dart';
|
||||||
|
part 'oauth_verification_confirm_request.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class OAuthVerificationConfirmRequest with _$OAuthVerificationConfirmRequest {
|
||||||
|
const factory OAuthVerificationConfirmRequest({
|
||||||
|
@JsonKey(name: 'access_token') required String accessToken,
|
||||||
|
@JsonKey(name: 'refresh_token') required String refreshToken,
|
||||||
|
@JsonKey(name: 'client_id') required String clientId,
|
||||||
|
required String provider,
|
||||||
|
required String id,
|
||||||
|
String? email,
|
||||||
|
String? phone,
|
||||||
|
required String code,
|
||||||
|
@JsonKey(name: 'verification_type') required String verificationType,
|
||||||
|
@JsonKey(name: 'first_name') required String firstName,
|
||||||
|
@JsonKey(name: 'last_name') required String lastName,
|
||||||
|
@JsonKey(name: 'date_of_birth') required String dateOfBirth,
|
||||||
|
required String gender,
|
||||||
|
required int weight,
|
||||||
|
required int height,
|
||||||
|
}) = _OAuthVerificationConfirmRequest;
|
||||||
|
|
||||||
|
factory OAuthVerificationConfirmRequest.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmRequestFromJson(json);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'oauth_verification_confirm_response.freezed.dart';
|
||||||
|
part 'oauth_verification_confirm_response.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class OAuthVerificationConfirmResponse with _$OAuthVerificationConfirmResponse {
|
||||||
|
const factory OAuthVerificationConfirmResponse({
|
||||||
|
required bool success,
|
||||||
|
required String message,
|
||||||
|
required String token,
|
||||||
|
required Map<String, dynamic> user,
|
||||||
|
@JsonKey(name: 'is_new_user') required bool isNewUser,
|
||||||
|
}) = _OAuthVerificationConfirmResponse;
|
||||||
|
|
||||||
|
factory OAuthVerificationConfirmResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationConfirmResponseFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'oauth_verification_request_response.freezed.dart';
|
||||||
|
part 'oauth_verification_request_response.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class OAuthVerificationRequestResponse with _$OAuthVerificationRequestResponse {
|
||||||
|
const factory OAuthVerificationRequestResponse({required String email, required String status}) = _OAuthVerificationRequestResponse;
|
||||||
|
|
||||||
|
factory OAuthVerificationRequestResponse.fromJson(Map<String, dynamic> json) => _$OAuthVerificationRequestResponseFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:eatvio_app/core/firebase_api.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
|
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart';
|
||||||
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: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';
|
||||||
|
|
||||||
class AuthCubit extends Cubit<AuthState> {
|
class AuthCubit extends Cubit<AuthState> {
|
||||||
final AuthRepository authRepository;
|
final AuthRepository authRepository;
|
||||||
|
final ProfileRepository profileRepository;
|
||||||
|
final FirebaseApi firebaseApi;
|
||||||
|
final Prefs prefs;
|
||||||
|
|
||||||
AuthCubit({required this.authRepository}) : super(const AuthState());
|
AuthCubit({required this.authRepository, required this.prefs, required this.profileRepository, required this.firebaseApi}) : super(const AuthState());
|
||||||
|
|
||||||
Future<bool> getTokenByCredentials(String email, String password) async {
|
Future<bool> getTokenByCredentials(String email, String password) async {
|
||||||
emit(const AuthState.loading());
|
emit(const AuthState.loading());
|
||||||
@@ -18,11 +27,42 @@ 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'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> initializeUser() async {
|
||||||
|
try {
|
||||||
|
await registerAnonymousPushToken();
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
} catch (e, st) {
|
||||||
|
log('Error in initializeUser: $e', stackTrace: st);
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerAnonymousPushToken() async {
|
||||||
|
try {
|
||||||
|
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
||||||
|
if (!hasAnonymousTokenBeenSent) {
|
||||||
|
await firebaseApi.registerAnonymousPushToken();
|
||||||
|
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> registerPushToken() async {
|
||||||
|
try {
|
||||||
|
await firebaseApi.registerPushToken();
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
||||||
import 'package:eatvio_app/core/prefs.dart';
|
import 'package:eatvio_app/core/prefs.dart';
|
||||||
import 'package:eatvio_app/core/token_storage.dart';
|
import 'package:eatvio_app/core/token_storage.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart';
|
||||||
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;
|
||||||
@@ -28,7 +30,42 @@ 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));
|
||||||
} catch (e) {
|
} on DioException catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log('OAuthCubit: Error occurred: $e');
|
||||||
|
// Проверяем, является ли ошибка ошибкой валидации email/phone
|
||||||
|
final errors = e.response?.data['errors'] as Map<String, dynamic>?;
|
||||||
|
final emailErrors = errors?['email'] as List<dynamic>?;
|
||||||
|
final phoneErrors = errors?['phone'] as List<dynamic>?;
|
||||||
|
|
||||||
|
final errorsList = <RegistrationValidationErrors>[];
|
||||||
|
if (emailErrors != null && emailErrors.isNotEmpty) {
|
||||||
|
for (final error in emailErrors) {
|
||||||
|
final errorStr = error.toString();
|
||||||
|
if (errorStr.contains('Некорректный формат email')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
|
}
|
||||||
|
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||||
|
for (final error in phoneErrors) {
|
||||||
|
final errorStr = error.toString();
|
||||||
|
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorsList.isNotEmpty) {
|
||||||
|
emit(OAuthState.error(errorMessage: 'validation_error', registrationValidationErrors: errorsList));
|
||||||
|
} else {
|
||||||
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
} 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()));
|
||||||
}
|
}
|
||||||
@@ -69,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>?;
|
||||||
@@ -77,16 +115,22 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
|
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
|
||||||
final errorsList = <RegistrationValidationErrors>[];
|
final errorsList = <RegistrationValidationErrors>[];
|
||||||
if (emailErrors != null && emailErrors.isNotEmpty) {
|
if (emailErrors != null && emailErrors.isNotEmpty) {
|
||||||
if (emailErrors.contains('Некорректный формат email')) {
|
for (final error in emailErrors) {
|
||||||
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
final errorStr = error.toString();
|
||||||
}
|
if (errorStr.contains('Некорректный формат email')) {
|
||||||
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
|
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||||
errorsList.add(RegistrationValidationErrors.emailExist);
|
}
|
||||||
|
if (errorStr.contains('почта уже существует') || errorStr.contains('Эл. почта уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
if (phoneErrors != null && phoneErrors.isNotEmpty) {
|
||||||
if (phoneErrors.contains('Такой phone уже существует.')) {
|
for (final error in phoneErrors) {
|
||||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
final errorStr = error.toString();
|
||||||
|
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
||||||
|
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
||||||
@@ -101,6 +145,71 @@ class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
|
||||||
|
try {
|
||||||
|
emit(const OAuthState.loading());
|
||||||
|
log('OAuthCubit: Requesting verification code for email: $email, phone: $phone');
|
||||||
|
final response = await authRepository.requestOAuthVerification(email: email, phone: phone);
|
||||||
|
if (response != null) {
|
||||||
|
log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}');
|
||||||
|
}
|
||||||
|
// Сбрасываем состояние после успешного запроса кода
|
||||||
|
emit(const OAuthState());
|
||||||
|
return response;
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log('OAuthCubit: Error requesting verification code: $e');
|
||||||
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> confirmOAuthVerification({
|
||||||
|
required String provider,
|
||||||
|
required String accessToken,
|
||||||
|
required String refreshToken,
|
||||||
|
required String clientId,
|
||||||
|
required String id,
|
||||||
|
String? email,
|
||||||
|
String? phone,
|
||||||
|
required String code,
|
||||||
|
required String verificationType,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
required String dateOfBirth,
|
||||||
|
required String gender,
|
||||||
|
required int weight,
|
||||||
|
required int height,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
emit(const OAuthState.loading());
|
||||||
|
log('OAuthCubit: Confirming OAuth verification - provider: $provider, verificationType: $verificationType, code: $code');
|
||||||
|
final response = await authRepository.confirmOAuthVerification(
|
||||||
|
provider: provider,
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
clientId: clientId,
|
||||||
|
id: id,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
code: code,
|
||||||
|
verificationType: verificationType,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
dateOfBirth: dateOfBirth,
|
||||||
|
gender: gender,
|
||||||
|
weight: weight,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
log('OAuthCubit: OAuth verification confirmed successfully, token received');
|
||||||
|
TokenStorage.saveTokenInStorage(response.token);
|
||||||
|
emit(OAuthState.loaded(token: response.token));
|
||||||
|
} catch (e, st) {
|
||||||
|
await reportFrom(e, st, this);
|
||||||
|
log('OAuthCubit: Error confirming OAuth verification: $e');
|
||||||
|
emit(OAuthState.error(errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }
|
|
||||||
|
|||||||
@@ -10,5 +10,10 @@ abstract class OAuthState with _$OAuthState {
|
|||||||
|
|
||||||
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
|
const factory OAuthState.loaded({required String token}) = OAuthStateLoaded;
|
||||||
|
|
||||||
const factory OAuthState.error({required String errorMessage}) = OAuthStateError;
|
const factory OAuthState.error({
|
||||||
|
required String errorMessage,
|
||||||
|
List<RegistrationValidationErrors>? registrationValidationErrors,
|
||||||
|
}) = OAuthStateError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,12 @@ 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';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@@ -81,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) {
|
||||||
@@ -214,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) {
|
||||||
@@ -232,9 +224,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
|||||||
final bool useMobileLayout = shortestSide < 600;
|
final bool useMobileLayout = shortestSide < 600;
|
||||||
if (!useMobileLayout) isFocused = true;
|
if (!useMobileLayout) isFocused = true;
|
||||||
final s = S.of(context);
|
final s = S.of(context);
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
return BlocProvider<AuthCubit>(
|
return BlocProvider<AuthCubit>(
|
||||||
create: (BuildContext context) => authCubit,
|
create: (BuildContext context) => authCubit..initializeUser(),
|
||||||
child: BlocBuilder<AuthCubit, AuthState>(
|
child: BlocBuilder<AuthCubit, AuthState>(
|
||||||
builder: (BuildContext context, AuthState state) {
|
builder: (BuildContext context, AuthState state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
|||||||
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
|
import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart';
|
||||||
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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@@ -29,14 +30,9 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
||||||
builder: (BuildContext context, ResetPasswordState state) {
|
builder: (BuildContext context, ResetPasswordState state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), centerTitle: false),
|
||||||
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
|
|
||||||
centerTitle: false,
|
|
||||||
),
|
|
||||||
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
||||||
? const Center(
|
? const Center(child: CircularProgressIndicator())
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
)
|
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -45,63 +41,32 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
||||||
Text(
|
Text(
|
||||||
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
const Text(
|
const Text('E-mail', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'E-mail',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(controller: emailController, hintText: 'Введите e-mail'),
|
||||||
controller: emailController,
|
|
||||||
hintText: 'Введите e-mail',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
||||||
const Text(
|
const Text('Код', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'Код',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(controller: codeController, hintText: 'Введите код'),
|
||||||
controller: codeController,
|
|
||||||
hintText: 'Введите код',
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text('Пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'Пароль',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
hintText: 'Введите новый пароль',
|
hintText: 'Введите новый пароль',
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text('Подтвердите пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
'Подтвердите пароль',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(
|
CustomTextFormField(
|
||||||
controller: passwordConfirmController,
|
controller: passwordConfirmController,
|
||||||
hintText: 'Введите новый пароль',
|
hintText: 'Введите новый пароль',
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@@ -109,11 +74,11 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
||||||
context.read<ResetPasswordCubit>().tryResetPassword(
|
context.read<ResetPasswordCubit>().tryResetPassword(
|
||||||
emailController.text,
|
emailController.text,
|
||||||
codeController.text,
|
codeController.text,
|
||||||
passwordController.text,
|
passwordController.text,
|
||||||
passwordConfirmController.text,
|
passwordConfirmController.text,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
||||||
}
|
}
|
||||||
@@ -122,20 +87,13 @@ class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
|||||||
child: Ink(
|
child: Ink(
|
||||||
width: MediaQuery.of(context).size.width - 32,
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
color: appTheme.blueA400,
|
|
||||||
),
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
||||||
? 'Сменить пароль'
|
? 'Сменить пароль'
|
||||||
: 'Сбросить пароль',
|
: 'Сбросить пароль',
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/const.dart';
|
import 'package:eatvio_app/core/const.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
@@ -12,9 +13,9 @@ 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:flutter/gestures.dart';
|
import 'package:flutter/gestures.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';
|
||||||
@@ -49,6 +50,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
bool weightError = false;
|
bool weightError = false;
|
||||||
bool heightError = false;
|
bool heightError = false;
|
||||||
bool incorrectEmailFormatError = false;
|
bool incorrectEmailFormatError = false;
|
||||||
|
bool showVerificationButton = false;
|
||||||
|
String? verificationType; // 'email' or 'phone'
|
||||||
|
|
||||||
// Состояние для выбора пола
|
// Состояние для выбора пола
|
||||||
bool isMaleSelected = true;
|
bool isMaleSelected = true;
|
||||||
@@ -70,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) {
|
||||||
@@ -221,7 +221,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
hasError: emailExistError || incorrectEmailFormatError,
|
hasError: emailExistError || incorrectEmailFormatError,
|
||||||
onChanged: (newValue) {},
|
onChanged: (newValue) {},
|
||||||
),
|
),
|
||||||
if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')],
|
if (emailExistError) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const ValidationErrorWidget(error: 'Такая почта уже существует.'),
|
||||||
|
if (showVerificationButton && verificationType == 'email') ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
onTap: () => _handleVerificationRequest(context, 'email'),
|
||||||
|
child: Ink(
|
||||||
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Да, это мой аккаунт, подтвердить',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
|
if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text('Номер телефона'),
|
const Text('Номер телефона'),
|
||||||
@@ -235,7 +256,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
hasError: phoneExistError,
|
hasError: phoneExistError,
|
||||||
onChanged: (newValue) {},
|
onChanged: (newValue) {},
|
||||||
),
|
),
|
||||||
if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')],
|
if (phoneExistError) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const ValidationErrorWidget(error: 'Такой номер телефона уже существует.'),
|
||||||
|
if (showVerificationButton && verificationType == 'phone') ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
onTap: () => _handleVerificationRequest(context, 'phone'),
|
||||||
|
child: Ink(
|
||||||
|
width: MediaQuery.of(context).size.width - 32,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: const Center(
|
||||||
|
child: Text(
|
||||||
|
'Да, это мой аккаунт, подтвердить',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -289,12 +331,17 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
const Text('Вес'),
|
const Text('Рост'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
|
CustomTextFormField(
|
||||||
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
|
controller: heightController,
|
||||||
|
hintText: 'Введите рост',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (newValue) {},
|
||||||
|
hasError: heightError,
|
||||||
|
),
|
||||||
|
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -303,11 +350,18 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Text('Рост'),
|
const Text('Вес'),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
|
CustomTextFormField(
|
||||||
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
|
controller: weightController,
|
||||||
|
hintText: 'Введите вес',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (newValue) {},
|
||||||
|
hasError: weightError,
|
||||||
|
),
|
||||||
|
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -481,7 +535,7 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
if (hasErrorWeightOrHeightError) return;
|
if (hasErrorWeightOrHeightError) return;
|
||||||
|
|
||||||
// Очищаем номер телефона от маски для отправки
|
// Очищаем номер телефона от маски для отправки
|
||||||
final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
|
final cleanPhone = ValidationUtils.normalizePhone(phoneController.text);
|
||||||
|
|
||||||
if (clientID == null) return;
|
if (clientID == null) return;
|
||||||
// Вызываем OAuth регистрацию
|
// Вызываем OAuth регистрацию
|
||||||
@@ -501,6 +555,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
clientId: clientID,
|
clientId: clientID,
|
||||||
);
|
);
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
|
bool hasEmailExist = false;
|
||||||
|
bool hasPhoneExist = false;
|
||||||
|
|
||||||
|
// Сначала определяем, какие ошибки присутствуют
|
||||||
for (final error in errors) {
|
for (final error in errors) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case RegistrationValidationErrors.incorrectEmailFormat:
|
case RegistrationValidationErrors.incorrectEmailFormat:
|
||||||
@@ -509,14 +567,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.emailExist:
|
case RegistrationValidationErrors.emailExist:
|
||||||
setState(() {
|
hasEmailExist = true;
|
||||||
emailExistError = true;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.phoneExist:
|
case RegistrationValidationErrors.phoneExist:
|
||||||
setState(() {
|
hasPhoneExist = true;
|
||||||
phoneExistError = true;
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -525,8 +579,236 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Устанавливаем verificationType согласно правилам:
|
||||||
|
// - Если оба (email и phone) существуют -> 'email'
|
||||||
|
// - Если только email -> 'email'
|
||||||
|
// - Если только phone -> 'phone'
|
||||||
|
if (hasEmailExist || hasPhoneExist) {
|
||||||
|
setState(() {
|
||||||
|
emailExistError = hasEmailExist;
|
||||||
|
phoneExistError = hasPhoneExist;
|
||||||
|
showVerificationButton = true;
|
||||||
|
if (hasEmailExist && hasPhoneExist) {
|
||||||
|
verificationType = 'email'; // Если оба существуют, используем email
|
||||||
|
} else if (hasEmailExist) {
|
||||||
|
verificationType = 'email'; // Только email
|
||||||
|
} else {
|
||||||
|
verificationType = 'phone'; // Только phone
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если есть ошибки, возвращаемся и не показываем toast об успехе
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Регистрация успешна только если нет ошибок
|
||||||
showSuccessToast('Регистрация успешна');
|
showSuccessToast('Регистрация успешна');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleVerificationRequest(BuildContext context, String type) async {
|
||||||
|
try {
|
||||||
|
// Используем service locator для доступа к OAuthCubit
|
||||||
|
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
|
||||||
|
// Всегда передаем и email и phone
|
||||||
|
final String email = emailController.text;
|
||||||
|
final String phone = ValidationUtils.normalizePhone(phoneController.text);
|
||||||
|
|
||||||
|
final response = await oauthCubit.requestOAuthVerification(email: email, phone: phone);
|
||||||
|
|
||||||
|
if (response != null && context.mounted) {
|
||||||
|
// Показываем диалог для ввода кода
|
||||||
|
final displayText = response.email;
|
||||||
|
_showVerificationCodeDialog(context, type, displayText);
|
||||||
|
} else if ((emailController.text.isEmpty || phoneController.text.isEmpty) && context.mounted) {
|
||||||
|
showErrorToast('Пожалуйста, заполните все поля');
|
||||||
|
} else if (context.mounted) {
|
||||||
|
showErrorToast('Не удалось отправить код верификации');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
showErrorToast('Ошибка: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showVerificationCodeDialog(BuildContext context, String verificationType, String displayText) {
|
||||||
|
final List<FocusNode> focusNodes = List.generate(4, (_) => FocusNode());
|
||||||
|
final List<TextEditingController> controllers = List.generate(4, (_) => TextEditingController());
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Подтверждение аккаунта'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Код отправлен на $displayText'),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: List.generate(4, (index) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 50,
|
||||||
|
child: TextField(
|
||||||
|
controller: controllers[index],
|
||||||
|
focusNode: focusNodes[index],
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 1,
|
||||||
|
decoration: const InputDecoration(counterText: '', border: OutlineInputBorder()),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isNotEmpty && index < 3) {
|
||||||
|
focusNodes[index + 1].requestFocus();
|
||||||
|
}
|
||||||
|
if (value.isEmpty && index > 0) {
|
||||||
|
focusNodes[index - 1].requestFocus();
|
||||||
|
}
|
||||||
|
// Проверяем, заполнен ли весь код
|
||||||
|
final code = controllers.map((c) => c.text).join();
|
||||||
|
if (code.length == 4) {
|
||||||
|
// Небольшая задержка перед отправкой, чтобы пользователь увидел последний символ
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () async {
|
||||||
|
if (dialogContext.mounted && context.mounted) {
|
||||||
|
await _handleVerificationConfirm(context, dialogContext, verificationType, code, controllers, focusNodes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
for (var controller in controllers) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
for (var node in focusNodes) {
|
||||||
|
node.dispose();
|
||||||
|
}
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Отмена'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleVerificationConfirm(
|
||||||
|
BuildContext widgetContext,
|
||||||
|
BuildContext dialogContext,
|
||||||
|
String verificationType,
|
||||||
|
String code,
|
||||||
|
List<TextEditingController> controllers,
|
||||||
|
List<FocusNode> focusNodes,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// Используем service locator для доступа к OAuthCubit, так как контекст из диалога может не иметь доступа к провайдеру
|
||||||
|
final oauthCubit = CubitRegistrations.sl<OAuthCubit>();
|
||||||
|
final authResult = widget.authResult;
|
||||||
|
|
||||||
|
if (authResult == null) {
|
||||||
|
showErrorToast('Ошибка: данные авторизации не найдены');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всегда передаем и email и phone (полный набор данных от провайдера)
|
||||||
|
// Используем данные из формы, если они есть, иначе из authResult
|
||||||
|
final String email = emailController.text.isNotEmpty ? emailController.text : (authResult.email ?? '');
|
||||||
|
final String phone = phoneController.text.isNotEmpty ? ValidationUtils.normalizePhone(phoneController.text) : ValidationUtils.normalizePhone(authResult.phone ?? '');
|
||||||
|
|
||||||
|
// Проверяем, что данные есть
|
||||||
|
if (email.isEmpty && phone.isEmpty) {
|
||||||
|
log('OAuth Verification Confirm: Both email and phone are empty');
|
||||||
|
showErrorToast('Ошибка: не указан email или телефон');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что все необходимые данные от провайдера есть
|
||||||
|
if (authResult.accessToken.isEmpty || authResult.clientId.isEmpty || authResult.userId.isEmpty) {
|
||||||
|
log('OAuth Verification Confirm: Missing required OAuth data');
|
||||||
|
showErrorToast('Ошибка: неполные данные авторизации');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что код не пустой
|
||||||
|
if (code.isEmpty || code.length != 4) {
|
||||||
|
showErrorToast('Введите полный код');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем данные профиля из формы
|
||||||
|
final String firstName = firstNameController.text.isNotEmpty ? firstNameController.text : (authResult.firstName ?? '');
|
||||||
|
final String lastName = lastNameController.text.isNotEmpty ? lastNameController.text : (authResult.lastName ?? '');
|
||||||
|
final String dateOfBirth = dateOfBirthController.text.isNotEmpty ? dateOfBirthController.text : (authResult.birthDate ?? '');
|
||||||
|
final String gender = isMaleSelected ? 'male' : 'female';
|
||||||
|
final int weight = weightController.text.isNotEmpty ? (int.tryParse(weightController.text) ?? 0) : 0;
|
||||||
|
final int height = heightController.text.isNotEmpty ? (int.tryParse(heightController.text) ?? 0) : 0;
|
||||||
|
|
||||||
|
// Проверяем, что все обязательные поля заполнены
|
||||||
|
if (firstName.isEmpty || lastName.isEmpty || dateOfBirth.isEmpty || weight == 0 || height == 0) {
|
||||||
|
log('OAuth Verification Confirm: Missing required profile data');
|
||||||
|
showErrorToast('Ошибка: заполните все обязательные поля');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем перед отправкой запроса
|
||||||
|
log('OAuth Verification Confirm: provider=${widget.provider}, verificationType=$verificationType, code=$code, email=$email, phone=$phone');
|
||||||
|
|
||||||
|
// Отправляем запрос на подтверждение с полным набором данных от провайдера
|
||||||
|
await oauthCubit.confirmOAuthVerification(
|
||||||
|
provider: widget.provider ?? 'social',
|
||||||
|
accessToken: authResult.accessToken,
|
||||||
|
refreshToken: authResult.refreshToken ?? '',
|
||||||
|
clientId: authResult.clientId,
|
||||||
|
id: authResult.userId,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
code: code,
|
||||||
|
verificationType: verificationType,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
dateOfBirth: dateOfBirth,
|
||||||
|
gender: gender,
|
||||||
|
weight: weight,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Закрываем диалог
|
||||||
|
for (var controller in controllers) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
for (var node in focusNodes) {
|
||||||
|
node.dispose();
|
||||||
|
}
|
||||||
|
if (dialogContext.mounted) {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перезагружаем веб-сокет и переходим на главный экран
|
||||||
|
if (widgetContext.mounted) {
|
||||||
|
_reconnectWebSocket(widgetContext);
|
||||||
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
log('OAuth Verification Confirm Error: $e', error: e, stackTrace: stackTrace);
|
||||||
|
if (widgetContext.mounted) {
|
||||||
|
showErrorToast('Неверный код. Попробуйте снова.');
|
||||||
|
}
|
||||||
|
// Очищаем поля
|
||||||
|
for (var controller in controllers) {
|
||||||
|
controller.clear();
|
||||||
|
}
|
||||||
|
if (focusNodes.isNotEmpty) {
|
||||||
|
focusNodes[0].requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_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';
|
||||||
|
|
||||||
@@ -10,15 +14,11 @@ 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) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Future<TokenModel> signInViaSocialMedia({
|
Future<TokenModel> signInViaSocialMedia({
|
||||||
required String provider,
|
required String provider,
|
||||||
@@ -26,13 +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) {
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> signUpViaSocialMedia({
|
Future<String> signUpViaSocialMedia({
|
||||||
@@ -50,27 +47,80 @@ 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) {
|
}
|
||||||
rethrow;
|
|
||||||
|
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
|
||||||
|
Map<String, dynamic>? body;
|
||||||
|
if (email != null) {
|
||||||
|
body = {'email': email};
|
||||||
|
} else if (phone != null) {
|
||||||
|
body = {'phone': phone};
|
||||||
}
|
}
|
||||||
|
if (body != null) {
|
||||||
|
return await authService.requestOAuthVerification(body: body);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({
|
||||||
|
required String provider,
|
||||||
|
required String accessToken,
|
||||||
|
required String refreshToken,
|
||||||
|
required String clientId,
|
||||||
|
required String id,
|
||||||
|
String? email,
|
||||||
|
String? phone,
|
||||||
|
required String code,
|
||||||
|
required String verificationType,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
required String dateOfBirth,
|
||||||
|
required String gender,
|
||||||
|
required int weight,
|
||||||
|
required int height,
|
||||||
|
}) async {
|
||||||
|
final request = OAuthVerificationConfirmRequest(
|
||||||
|
provider: provider,
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
clientId: clientId,
|
||||||
|
id: id,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
code: code,
|
||||||
|
verificationType: verificationType,
|
||||||
|
firstName: firstName,
|
||||||
|
lastName: lastName,
|
||||||
|
dateOfBirth: dateOfBirth,
|
||||||
|
gender: gender,
|
||||||
|
weight: weight,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
// Логируем запрос перед отправкой
|
||||||
|
log(
|
||||||
|
'AuthRepository: Sending OAuth verification confirm request: provider=$provider, verificationType=$verificationType, code=$code, email=$email, phone=$phone',
|
||||||
|
);
|
||||||
|
return await authService.confirmOAuthVerification(body: request);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,21 +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) {
|
|
||||||
rethrow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> confirmPasswordChange(
|
Future<bool> confirmPasswordChange(
|
||||||
@@ -31,23 +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) {
|
|
||||||
rethrow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ 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: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:vk_id/vk_id.dart';
|
import 'package:vk_id/vk_id.dart';
|
||||||
@@ -40,7 +43,8 @@ class SocialAuthFacade {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,8 +160,9 @@ class SocialAuthFacade {
|
|||||||
birthDate: yandexProfileData.birthday,
|
birthDate: yandexProfileData.birthday,
|
||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
|
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -204,7 +209,8 @@ class SocialAuthFacade {
|
|||||||
if (code == null || deviceId == null) return null;
|
if (code == null || deviceId == null) return null;
|
||||||
|
|
||||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
|
await sl<ErrorReporter>().report(e, s, origin: 'SocialAuthFacade');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,8 +261,9 @@ class SocialAuthFacade {
|
|||||||
if (!context.mounted) return null;
|
if (!context.mounted) return null;
|
||||||
return showDialog<Map<String, String>>(
|
return showDialog<Map<String, String>>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: true,
|
||||||
builder: (_) {
|
builder: (dialogContext) {
|
||||||
|
final s = S.of(dialogContext);
|
||||||
const borderRadius = BorderRadius.all(Radius.circular(16));
|
const borderRadius = BorderRadius.all(Radius.circular(16));
|
||||||
return Dialog(
|
return Dialog(
|
||||||
insetPadding: const EdgeInsets.all(16),
|
insetPadding: const EdgeInsets.all(16),
|
||||||
@@ -264,7 +271,21 @@ class SocialAuthFacade {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: SizedBox(height: 560, child: WebViewWidget(controller: controller)),
|
child: SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.7,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(s.authorization),
|
||||||
|
),
|
||||||
|
body: WebViewWidget(controller: controller),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
import 'package:eatvio_app/features/auth/models/credentials_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_model.dart';
|
||||||
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
import 'package:eatvio_app/features/auth/models/oauth_login_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart';
|
||||||
|
import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_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:retrofit/retrofit.dart';
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
||||||
@@ -19,4 +22,10 @@ abstract class AuthService {
|
|||||||
|
|
||||||
@POST('/api/oauth/login')
|
@POST('/api/oauth/login')
|
||||||
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
|
Future<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
|
||||||
|
|
||||||
|
@POST('/api/oauth/verification/request')
|
||||||
|
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required Map<String, dynamic> body});
|
||||||
|
|
||||||
|
@POST('/api/oauth/verification/confirm')
|
||||||
|
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body});
|
||||||
}
|
}
|
||||||
|
|||||||
29
lib/features/barcode/models/barcode_product_model.dart
Normal file
29
lib/features/barcode/models/barcode_product_model.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'barcode_product_model.freezed.dart';
|
||||||
|
part 'barcode_product_model.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class BarcodeProductModel with _$BarcodeProductModel {
|
||||||
|
const factory BarcodeProductModel({
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
required String GTIN,
|
||||||
|
@JsonKey(name: 'Товарный знак (ru)') String? brandName,
|
||||||
|
@JsonKey(name: 'Описание (ru)') String? description,
|
||||||
|
@JsonKey(name: 'Количество/вес') String? quantity,
|
||||||
|
@JsonKey(name: 'Рынок сбыта') String? market,
|
||||||
|
@JsonKey(name: 'Калорийность') double? calories,
|
||||||
|
@JsonKey(name: 'Белки') double? proteins,
|
||||||
|
@JsonKey(name: 'Жиры') double? fats,
|
||||||
|
@JsonKey(name: 'Углеводы') double? carbohydrates,
|
||||||
|
}) = _BarcodeProductModel;
|
||||||
|
|
||||||
|
factory BarcodeProductModel.fromJson(Map<String, Object?> json) => _$BarcodeProductModelFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class BarcodeTaskStatusModel with _$BarcodeTaskStatusModel {
|
||||||
|
const factory BarcodeTaskStatusModel({String? status, @JsonKey(name: 'task_id') String? taskId, BarcodeProductModel? data}) = _BarcodeTaskStatusModel;
|
||||||
|
|
||||||
|
factory BarcodeTaskStatusModel.fromJson(Map<String, Object?> json) => _$BarcodeTaskStatusModelFromJson(json);
|
||||||
|
}
|
||||||
107
lib/features/barcode/repositories/barcode_repository.dart
Normal file
107
lib/features/barcode/repositories/barcode_repository.dart
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
|
||||||
|
|
||||||
|
class BarcodeRepository {
|
||||||
|
final BarcodeService barcodeService;
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
BarcodeRepository({
|
||||||
|
required this.barcodeService,
|
||||||
|
required this.dio,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Получить информацию о продукте по штрихкоду
|
||||||
|
/// Возвращает либо данные продукта, либо статус задачи обработки
|
||||||
|
Future<BarcodeResult> getProductByBarcode(String barcode) async {
|
||||||
|
final response = await dio.get<Map<String, dynamic>>(
|
||||||
|
'/api/gsone/barcode/$barcode',
|
||||||
|
options: Options(validateStatus: (_) => true),
|
||||||
|
);
|
||||||
|
|
||||||
|
final code = response.statusCode ?? 0;
|
||||||
|
final data = response.data;
|
||||||
|
if (data is! Map<String, dynamic>) {
|
||||||
|
throw DioException(
|
||||||
|
requestOptions: response.requestOptions,
|
||||||
|
response: response,
|
||||||
|
message: 'Invalid barcode response',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
final statusModel = await barcodeService.getTaskStatus(taskId: taskId);
|
||||||
|
|
||||||
|
if (statusModel.status == 'completed' && statusModel.data != null) {
|
||||||
|
return BarcodeTaskStatus.completed(statusModel.data!);
|
||||||
|
} else if (statusModel.status == 'failed') {
|
||||||
|
return BarcodeTaskStatus.failed();
|
||||||
|
} else {
|
||||||
|
return BarcodeTaskStatus.processing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Результат запроса информации о продукте по штрихкоду
|
||||||
|
class BarcodeResult {
|
||||||
|
final BarcodeResultType type;
|
||||||
|
final BarcodeProductModel? product;
|
||||||
|
final String? taskId;
|
||||||
|
|
||||||
|
BarcodeResult._({required this.type, this.product, this.taskId});
|
||||||
|
|
||||||
|
factory BarcodeResult.success(BarcodeProductModel product) {
|
||||||
|
return BarcodeResult._(type: BarcodeResultType.success, product: product);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BarcodeResult.processing(String taskId) {
|
||||||
|
return BarcodeResult._(type: BarcodeResultType.processing, taskId: taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BarcodeResultType {
|
||||||
|
success,
|
||||||
|
processing,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Статус задачи обработки
|
||||||
|
class BarcodeTaskStatus {
|
||||||
|
final BarcodeTaskStatusType type;
|
||||||
|
final BarcodeProductModel? product;
|
||||||
|
|
||||||
|
BarcodeTaskStatus._({required this.type, this.product});
|
||||||
|
|
||||||
|
factory BarcodeTaskStatus.completed(BarcodeProductModel product) {
|
||||||
|
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.completed, product: product);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BarcodeTaskStatus.processing() {
|
||||||
|
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.processing);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory BarcodeTaskStatus.failed() {
|
||||||
|
return BarcodeTaskStatus._(type: BarcodeTaskStatusType.failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BarcodeTaskStatusType {
|
||||||
|
completed,
|
||||||
|
processing,
|
||||||
|
failed,
|
||||||
|
}
|
||||||
24
lib/features/barcode/services/barcode_service.dart
Normal file
24
lib/features/barcode/services/barcode_service.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:eatvio_app/features/barcode/models/barcode_product_model.dart';
|
||||||
|
import 'package:retrofit/retrofit.dart';
|
||||||
|
|
||||||
|
part 'barcode_service.g.dart';
|
||||||
|
|
||||||
|
@RestApi()
|
||||||
|
abstract class BarcodeService {
|
||||||
|
factory BarcodeService(Dio dio, {String baseUrl}) = _BarcodeService;
|
||||||
|
|
||||||
|
@GET('/api/gsone/barcode/{barcode}')
|
||||||
|
Future<BarcodeProductModel> getProductByBarcode({@Path('barcode') required String barcode});
|
||||||
|
|
||||||
|
@GET('/api/gsone/barcode/status/{task_id}')
|
||||||
|
Future<BarcodeTaskStatusModel> getTaskStatus({@Path('task_id') required String taskId});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:eatvio_app/core/app_config.dart';
|
import 'package:eatvio_app/core/app_config.dart';
|
||||||
import 'package:eatvio_app/core/enums/complained_content.dart';
|
import 'package:eatvio_app/core/enums/complained_content.dart';
|
||||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||||
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
import 'package:eatvio_app/core/widgets/custom_popup.dart';
|
||||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||||
|
import 'package:eatvio_app/core/widgets/markdown_html_widget.dart';
|
||||||
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
|
||||||
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/core/comments/comments_cubit/comments_cubit.dart';
|
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||||
@@ -13,17 +13,17 @@ import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit
|
|||||||
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/core/locator_service.dart';
|
import 'package:eatvio_app/core/locator_service.dart';
|
||||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||||
|
import 'package:eatvio_app/generated/l10n.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';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
|
||||||
|
|
||||||
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();
|
||||||
@@ -78,96 +78,102 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
bottomSheet: Container(
|
bottomSheet: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
|
padding: EdgeInsets.only(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))),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
children: [
|
child: Row(
|
||||||
if (replyId != null)
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
GestureDetector(
|
children: [
|
||||||
onTap: () {
|
if (replyId != null)
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
GestureDetector(
|
||||||
setState(() {
|
onTap: () {
|
||||||
replyId = null;
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
textEditingController.clear();
|
setState(() {
|
||||||
});
|
replyId = null;
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
textEditingController.clear();
|
||||||
},
|
});
|
||||||
child: Container(
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (replyId != null) const SizedBox(width: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width - (replyId == null ? 64 : 104),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minHeight: 52),
|
|
||||||
child: TextFormField(
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
onChanged: (_) {
|
|
||||||
setState(() {});
|
|
||||||
},
|
},
|
||||||
controller: textEditingController,
|
child: Container(
|
||||||
maxLines: 5,
|
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||||
minLines: 1,
|
height: 32,
|
||||||
keyboardType: TextInputType.multiline,
|
width: 32,
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
child: const Icon(Icons.close_rounded, size: 18, color: Colors.white),
|
||||||
decoration: InputDecoration(
|
),
|
||||||
isDense: false,
|
),
|
||||||
// убираем true, чтобы padding учитывался корректно
|
if (replyId != null) const SizedBox(width: 8),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
Expanded(
|
||||||
// вертикальный паддинг увеличен
|
child: ConstrainedBox(
|
||||||
hintText: 'Комментарий',
|
constraints: const BoxConstraints(minHeight: 52),
|
||||||
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
child: TextFormField(
|
||||||
filled: true,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
border: OutlineInputBorder(
|
onChanged: (_) {
|
||||||
borderRadius: BorderRadius.circular(16),
|
setState(() {});
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
},
|
||||||
),
|
controller: textEditingController,
|
||||||
enabledBorder: OutlineInputBorder(
|
maxLines: 5,
|
||||||
borderRadius: BorderRadius.circular(16),
|
minLines: 1,
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
keyboardType: TextInputType.multiline,
|
||||||
),
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1.2),
|
||||||
focusedBorder: OutlineInputBorder(
|
decoration: InputDecoration(
|
||||||
borderRadius: BorderRadius.circular(16),
|
isDense: false,
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
// убираем true, чтобы padding учитывался корректно
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
|
// вертикальный паддинг увеличен
|
||||||
|
hintText: S.of(context).comment_hint,
|
||||||
|
hintStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, height: 1.2, color: Color(0xFFAEAEB2)),
|
||||||
|
filled: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
GestureDetector(
|
||||||
GestureDetector(
|
onTap: () async {
|
||||||
onTap: () {
|
if (textEditingController.text.length > 2) {
|
||||||
if (textEditingController.text.length > 2) {
|
final isSent = await commentsCubit.sendComment(
|
||||||
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;
|
||||||
context.read<CommentsCubit>().setReplyId(null);
|
|
||||||
setState(() {
|
if (isSent) context.read<FeedCubit>().incrementCommentsState(widget.id);
|
||||||
textEditingController.clear();
|
|
||||||
replyId = null;
|
context.read<CommentsCubit>().setReplyId(null);
|
||||||
});
|
setState(() {
|
||||||
FocusManager.instance.primaryFocus?.unfocus();
|
textEditingController.clear();
|
||||||
}
|
replyId = null;
|
||||||
},
|
});
|
||||||
child: Container(
|
FocusManager.instance.primaryFocus?.unfocus();
|
||||||
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
|
}
|
||||||
height: 32,
|
},
|
||||||
width: 32,
|
child: Container(
|
||||||
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : Colors.grey, shape: BoxShape.circle),
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
child: const Icon(Icons.send_rounded, size: 18, color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -194,7 +200,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.only(bottom: 80),
|
padding: EdgeInsets.only(bottom: 160 + MediaQuery.of(context).padding.bottom), // поле комментария (52-200) + padding (12+12+56) + safe area
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -233,35 +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: Html(
|
child: MarkdownHtmlWidget(markdownText: blogPostState.blogPostModel!.description),
|
||||||
data: md.markdownToHtml(blogPostState.blogPostModel!.description.replaceAll('\n', '<br>')),
|
|
||||||
style: {
|
|
||||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
|
|
||||||
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
|
|
||||||
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
|
|
||||||
"h1": Style(fontSize: FontSize(24)),
|
|
||||||
"h2": Style(fontSize: FontSize(20)),
|
|
||||||
"h3": Style(fontSize: FontSize(18)),
|
|
||||||
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
|
|
||||||
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
|
|
||||||
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
|
|
||||||
"em, i": Style(fontStyle: FontStyle.italic),
|
|
||||||
"code": Style(
|
|
||||||
backgroundColor: Colors.grey.shade100,
|
|
||||||
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
|
|
||||||
"blockquote": Style(
|
|
||||||
padding: HtmlPaddings.only(left: 16),
|
|
||||||
margin: Margins.only(left: 8, bottom: 12),
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
|
|
||||||
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
|
if (blogPostState.blogPostModel?.image.isNotEmpty ?? false)
|
||||||
@@ -326,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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user