forked from eatvio/eatvio_app
Compare commits
289 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3892bf00aa | ||
|
|
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 | ||
|
|
8b1d08b2aa | ||
|
|
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 | |||
|
|
9e33063c30 | ||
|
|
f3d4478c65 | ||
| 673aa86370 | |||
|
|
1738d27b82 | ||
|
|
d0becb9e11 | ||
|
|
965ae2f02b | ||
|
|
9fcfac87d4 | ||
|
|
2a25be5aa4 | ||
| b420d11996 | |||
|
|
cbeb91a9f0 | ||
|
|
9ca09e8137 | ||
|
|
8917f54e7b | ||
|
|
626f74d20d | ||
|
|
7df32dabd2 | ||
|
|
2e57cd9519 | ||
|
|
c6c42b0525 | ||
|
|
58bb766721 | ||
|
|
0be7392a39 | ||
|
|
4deda92c5e | ||
|
|
b83d91e7f2 | ||
| e5029f581d | |||
|
|
c69fe7f0a6 | ||
| fc0e9b1063 | |||
|
|
453f119317 | ||
|
|
6d0bee4da8 | ||
|
|
04d4be38d4 | ||
| fa22cf5d36 | |||
| 91e8348015 | |||
|
|
1b948b99e3 | ||
|
|
b6eac3c46f | ||
|
|
6fa944b3df | ||
|
|
c31411c4a2 | ||
|
|
6214d19a14 | ||
|
|
db5dddff86 | ||
|
|
39775cd7a8 | ||
|
|
36e2f12e3f | ||
|
|
66d2533add | ||
|
|
425f16bfc9 | ||
|
|
c5e30d8f00 | ||
|
|
d0935fbaae | ||
| dda0aa7440 | |||
|
|
c81327b895 | ||
| 732d667ac1 | |||
|
|
ba9f4a3584 | ||
|
|
f63d901a55 | ||
|
|
9148a4c668 | ||
|
|
a3f0aa3e67 | ||
|
|
8a390c2a9b | ||
|
|
92b37e77b1 | ||
| 101c1e15af | |||
|
|
172c05ac69 | ||
|
|
4eecc458f8 | ||
|
|
4b781adb79 | ||
|
|
dc400ef5b7 | ||
|
|
d1160ba35f | ||
|
|
a46ac5bb4c | ||
|
|
992cb38cdd | ||
|
|
2a06a5476a | ||
|
|
f3853b58d9 | ||
|
|
bbc99ec9f4 | ||
| ff946a5938 | |||
|
|
a72c40d8ee | ||
|
|
6ed7faed15 | ||
|
|
12f51d49ea | ||
|
|
16fccaea8e | ||
|
|
a723673d23 | ||
|
|
89f697f7a6 | ||
| d883c9e19a | |||
|
|
884830f232 | ||
|
|
7b297f954e | ||
|
|
dbd92a65ea | ||
|
|
f3af743487 | ||
|
|
c95f1988ae | ||
|
|
35a5e6d6be | ||
|
|
977b7150fe | ||
| 5b81014e72 | |||
|
|
8799375880 | ||
|
|
fb5a0d52bc | ||
|
|
2e3172bc45 | ||
|
|
41313f09af | ||
|
|
e147f6e182 | ||
|
|
2440204294 | ||
|
|
46841fb502 | ||
|
|
8e72350152 | ||
|
|
8c2cd5eedd | ||
|
|
996f2ecd70 | ||
|
|
19787f459e | ||
|
|
7e64b24a8c | ||
|
|
a07d085ba7 | ||
|
|
5616bedd25 | ||
|
|
0a3d9e577d | ||
|
|
55e3abec99 |
@@ -3,19 +3,19 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.32.0"
|
||||
flutter_version: "3.41.2"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ dev-office ]
|
||||
runs-on: [ ubuntu-latest ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: https://github.com/actions/checkout@v3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
uses: https://github.com/actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: ${{ env.java_version }}
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Install Android SDK
|
||||
run: |
|
||||
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
|
||||
unzip commandlinetools.zip
|
||||
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
apt-get install -y jq
|
||||
|
||||
- id: flutter-action
|
||||
uses: actions/flutter-action@v2
|
||||
uses: https://github.com/subosito/flutter-action@v2
|
||||
with:
|
||||
channel: ${{ env.flutter_channel }}
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: https://github.com/actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.APK_NAME }}
|
||||
path: ${{ env.APK_NAME }}
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.32.0"
|
||||
flutter_version: "3.41.2"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: [ dev-office-flutter ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: https://github.com/actions/checkout@v3
|
||||
|
||||
- name: Set up Android environment variables
|
||||
id: "android-env-setup"
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
# echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||
|
||||
# - name: Upload APK artifact
|
||||
# uses: actions/upload-artifact@v3
|
||||
# uses: https://github.com/actions/upload-artifact@v3
|
||||
# with:
|
||||
# name: ${{ env.APK_NAME }}
|
||||
# path: ${{ env.APK_NAME }}
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
runs-on: [macos-arm64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: https://github.com/actions/checkout@v2
|
||||
|
||||
- name: Checkout env repository
|
||||
uses: actions/checkout@v2
|
||||
uses: https://github.com/actions/checkout@v2
|
||||
with:
|
||||
repository: mso7nueh/eatvio_app_env
|
||||
path: env_repo
|
||||
@@ -54,11 +54,13 @@ jobs:
|
||||
|
||||
- name: Flutter build
|
||||
run: |
|
||||
flutter upgrade
|
||||
flutter clean
|
||||
flutter pub get
|
||||
flutter pub upgrade
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# Configure Flutter for release mode
|
||||
pod repo update
|
||||
flutter build ios --config-only --release
|
||||
cd ios
|
||||
pod update
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.35.1"
|
||||
flutter_version: "3.41.2"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
@@ -11,13 +11,23 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ dev-office ]
|
||||
runs-on: [ ubuntu-latest ]
|
||||
|
||||
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
|
||||
uses: actions/checkout@v3
|
||||
uses: https://github.com/actions/checkout@v3
|
||||
with:
|
||||
repository: mso7nueh/eatvio_app_env
|
||||
path: env_repo
|
||||
@@ -56,13 +66,13 @@ jobs:
|
||||
|
||||
- name: Cache Android SDK
|
||||
id: cache-sdk
|
||||
uses: "https://gitea.com/actions/cache@v4"
|
||||
uses: "https://github.com/actions/cache@v4"
|
||||
with:
|
||||
path: ${{ env.my_local_cache_dir }}
|
||||
key: ${{ runner.os }}-cache-sdk
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
uses: https://github.com/actions/setup-java@v3
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: ${{ env.java_version }}
|
||||
@@ -71,7 +81,7 @@ jobs:
|
||||
- name: Install utils for Android SDK
|
||||
run: |
|
||||
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
|
||||
if: steps.cache-sdk.outputs.cache-hit != 'true'
|
||||
@@ -99,16 +109,23 @@ jobs:
|
||||
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
|
||||
|
||||
- id: flutter-action
|
||||
uses: actions/flutter-action@v2
|
||||
with:
|
||||
channel: ${{ env.flutter_channel }}
|
||||
flutter-version: ${{ env.flutter_version }}
|
||||
cache: true
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache
|
||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
|
||||
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache of dart pub get dependencies
|
||||
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path
|
||||
|
||||
- name: Set up Flutter
|
||||
run: |
|
||||
FLUTTER_DIR="${{ env.my_local_cache_dir }}flutter"
|
||||
if [ ! -f "$FLUTTER_DIR/bin/flutter" ]; then
|
||||
echo "Downloading Flutter ${{ env.flutter_version }}..."
|
||||
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"
|
||||
echo "Extracting Flutter..."
|
||||
mkdir -p "${{ env.my_local_cache_dir }}"
|
||||
tar xf /tmp/flutter.tar.xz -C "${{ env.my_local_cache_dir }}"
|
||||
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
|
||||
# shell: bash
|
||||
@@ -126,7 +143,9 @@ jobs:
|
||||
|
||||
- name: Fix dir permissions
|
||||
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 doctor -v
|
||||
|
||||
@@ -140,7 +159,69 @@ jobs:
|
||||
run: flutter analyze
|
||||
|
||||
- name: Test
|
||||
run: flutter test
|
||||
run: flutter test --coverage
|
||||
|
||||
- name: Test coverage
|
||||
run: |
|
||||
echo "📊 Analyzing test coverage..."
|
||||
echo ""
|
||||
awk -F: '
|
||||
/^SF:/ {
|
||||
file = $0
|
||||
sub(/^SF:/, "", file)
|
||||
sub(/^[[:space:]]+/, "", file)
|
||||
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 {
|
||||
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 < 60) {
|
||||
printf "⚠️ Warning: Coverage is below 60%%\n"
|
||||
} else if (percentage < 80) {
|
||||
printf "💛 Good: Coverage is above 60%%\n"
|
||||
} else {
|
||||
printf "💚 Excellent: Coverage is above 80%%\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
' coverage/lcov.info
|
||||
|
||||
- name: Build apk
|
||||
run: |
|
||||
@@ -158,7 +239,7 @@ jobs:
|
||||
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: https://github.com/actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.APK_NAME }}
|
||||
path: ${{ env.APK_NAME }}
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -19,4 +19,9 @@ ios/XCBuildData/
|
||||
env/.env
|
||||
.env
|
||||
android/key.properties
|
||||
app/upload-keystore.jks
|
||||
app/upload-keystore.jks
|
||||
coverage/
|
||||
backend/
|
||||
.fvm/
|
||||
.fvmrc
|
||||
/eatvio
|
||||
5
Makefile
5
Makefile
@@ -7,4 +7,7 @@ run-analyse:
|
||||
run-checks: run-analyse run-tests
|
||||
|
||||
build-generated-files:
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
clean-coverage:
|
||||
@./scripts/remove_generated_from_coverage.sh
|
||||
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'
|
||||
// END: FlutterFire Configuration
|
||||
id "kotlin-android"
|
||||
id "kotlin-parcelize"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
id 'com.huawei.agconnect'
|
||||
}
|
||||
@@ -19,15 +20,15 @@ def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property(
|
||||
android {
|
||||
namespace = "com.eatvioapp.app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -68,10 +69,10 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301'
|
||||
implementation 'com.huawei.hms:push:6.12.0.300'
|
||||
implementation 'com.huawei.agconnect:agconnect-core:1.9.5.300'
|
||||
implementation 'com.huawei.hms:push:6.13.0.301'
|
||||
// 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
|
||||
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
<meta-data
|
||||
android:name="push_kit_auto_init_enabled"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.eatvioapp.app
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
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.RemoteMessage
|
||||
|
||||
@@ -13,6 +17,45 @@ class HmsPushService : HmsMessageService() {
|
||||
override fun onMessageReceived(message: RemoteMessage?) {
|
||||
super.onMessageReceived(message)
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -14,12 +14,13 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
private val channelName = "com.eatvioapp/hms_push"
|
||||
private var methodChannel: MethodChannel? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
.setMethodCallHandler { call, result ->
|
||||
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
|
||||
methodChannel?.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getHuaweiToken" -> {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@@ -47,17 +48,21 @@ class MainActivity: FlutterActivity() {
|
||||
}
|
||||
|
||||
if (appId.isNullOrBlank()) {
|
||||
android.util.Log.e("HMS", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.")
|
||||
runOnUiThread {
|
||||
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
android.util.Log.i("HMS", "Getting token for appId: $appId")
|
||||
val token = HmsInstanceId.getInstance(applicationContext)
|
||||
.getToken(appId, "HCM")
|
||||
android.util.Log.i("HMS", "Token retrieved successfully")
|
||||
runOnUiThread {
|
||||
result.success(token)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("HMS", "Error getting HMS token: ${e.message}", e)
|
||||
runOnUiThread {
|
||||
result.error("HMS_TOKEN_ERROR", e.message, null)
|
||||
}
|
||||
@@ -67,5 +72,33 @@ class MainActivity: FlutterActivity() {
|
||||
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/check.svg
Normal file
3
assets/icons/check.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="9" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.16229 6.8625L10.5185 0.50625C10.6685 0.35625 10.8435 0.28125 11.0435 0.28125C11.2435 0.28125 11.4185 0.35625 11.5685 0.50625C11.7185 0.65625 11.7935 0.834375 11.7935 1.04063C11.7935 1.24688 11.7185 1.425 11.5685 1.575L4.68729 8.475C4.53729 8.625 4.36229 8.7 4.16229 8.7C3.96229 8.7 3.78729 8.625 3.63729 8.475L0.412289 5.25C0.262289 5.1 0.190414 4.92188 0.196664 4.71563C0.202914 4.50938 0.281039 4.33125 0.431039 4.18125C0.581039 4.03125 0.759164 3.95625 0.965414 3.95625C1.17166 3.95625 1.34979 4.03125 1.49979 4.18125L4.16229 6.8625Z" fill="#16A249"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 668 B |
11
assets/icons/check_filled.svg
Normal file
11
assets/icons/check_filled.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.2" width="20" height="20" rx="6" fill="#16A249"/>
|
||||
<g clip-path="url(#clip0_1823_10218)">
|
||||
<path d="M8.16229 12.8625L14.5185 6.50625C14.6685 6.35625 14.8435 6.28125 15.0435 6.28125C15.2435 6.28125 15.4185 6.35625 15.5685 6.50625C15.7185 6.65625 15.7935 6.83438 15.7935 7.04063C15.7935 7.24688 15.7185 7.425 15.5685 7.575L8.68729 14.475C8.53729 14.625 8.36229 14.7 8.16229 14.7C7.96229 14.7 7.78729 14.625 7.63729 14.475L4.41229 11.25C4.26229 11.1 4.19041 10.9219 4.19666 10.7156C4.20291 10.5094 4.28104 10.3313 4.43104 10.1813C4.58104 10.0313 4.75916 9.95625 4.96541 9.95625C5.17166 9.95625 5.34979 10.0313 5.49979 10.1813L8.16229 12.8625Z" fill="#16A249"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1823_10218">
|
||||
<rect width="12" height="9" fill="white" transform="translate(4 6)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 911 B |
12
assets/icons/close_mark.svg
Normal file
12
assets/icons/close_mark.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1846_342)">
|
||||
<path d="M5.33333 16C3.85185 16 2.59259 15.4815 1.55556 14.4444C0.518518 13.4074 0 12.1481 0 10.6667V5.33333C0 3.85185 0.518518 2.59259 1.55556 1.55556C2.59259 0.518518 3.85185 0 5.33333 0H10.6667C12.1481 0 13.4074 0.518518 14.4444 1.55556C15.4815 2.59259 16 3.85185 16 5.33333V10.6667C16 12.1481 15.4815 13.4074 14.4444 14.4444C13.4074 15.4815 12.1481 16 10.6667 16H5.33333ZM5.33333 14.2222H10.6667C11.6444 14.2222 12.4815 13.8741 13.1778 13.1778C13.8741 12.4815 14.2222 11.6444 14.2222 10.6667V5.33333C14.2222 4.35556 13.8741 3.51852 13.1778 2.82222C12.4815 2.12593 11.6444 1.77778 10.6667 1.77778H5.33333C4.35556 1.77778 3.51852 2.12593 2.82222 2.82222C2.12593 3.51852 1.77778 4.35556 1.77778 5.33333V10.6667C1.77778 11.6444 2.12593 12.4815 2.82222 13.1778C3.51852 13.8741 4.35556 14.2222 5.33333 14.2222Z" fill="#D22121"/>
|
||||
<rect x="4.81836" y="10.9749" width="8" height="1" rx="0.5" transform="rotate(-45 4.81836 10.9749)" fill="#D22121"/>
|
||||
<rect x="5.52539" y="5.31799" width="8" height="1" rx="0.5" transform="rotate(45 5.52539 5.31799)" fill="#D22121"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1846_342">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
9
assets/icons/logo_2.svg
Normal file
9
assets/icons/logo_2.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="89" height="90" viewBox="0 0 89 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.0119 0C65.9947 0 83 9.05026 83 29.6764C83 50.2792 65.9947 67 45.0119 67C24.0053 67 7 50.2792 7 29.6764C7 9.05026 24.0053 0 45.0119 0Z" fill="#43A6F2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.8844 43.5162C39.8585 43.5162 34.9274 42.5623 30.3282 40.7209C25.7291 38.8573 21.5803 36.1284 18.1428 32.7119C16.5544 31.1589 16.6493 28.6741 18.3087 27.2098C19.9919 25.7234 22.6234 25.8122 24.2118 27.3651C26.867 30.0052 30.0675 32.1129 33.6235 33.5327C37.1796 34.9748 40.9964 35.7069 44.8844 35.7069C48.7486 35.7069 52.5655 34.9748 56.1215 33.5327C59.6776 32.1129 62.878 30.0052 65.5332 27.3651C67.1216 25.8122 69.7531 25.7234 71.4363 27.2098C73.0958 28.6741 73.1906 31.1589 71.6022 32.7119C68.1647 36.1284 64.016 38.8573 59.4168 40.7209C54.8176 42.5623 49.8866 43.5162 44.8844 43.5162Z" fill="white"/>
|
||||
<path d="M9.69721 80.8957L3.51318 88.877C2.93793 89.6257 2.28049 90 1.54087 90C0.513623 90 0 89.3382 0 88.0147V77.3663C0 75.7888 0.56841 75 1.70523 75C2.84205 75 3.41046 75.7888 3.41046 77.3663V84.2246L9.71775 76.0829C10.2793 75.361 10.882 75 11.5257 75C12.5803 75 13.1077 75.7487 13.1077 77.246V87.6337C13.1077 89.2112 12.5393 90 11.4024 90C10.2656 90 9.69721 89.2112 9.69721 87.6337V80.8957Z" fill="#323F4B"/>
|
||||
<path d="M23.0017 78.2888C21.769 78.2888 21.0019 78.2553 20.7006 78.1885C19.7829 78.0013 19.3241 77.5201 19.3241 76.7447C19.3241 75.7019 20.1322 75.1805 21.7484 75.1805H27.6859C29.3021 75.1805 30.1102 75.7019 30.1102 76.7447C30.1102 77.5201 29.6514 78.0013 28.7337 78.1885C28.4324 78.2553 27.6654 78.2888 26.4327 78.2888V87.6337C26.4327 89.2112 25.8642 90 24.7274 90C23.5769 90 23.0017 89.2112 23.0017 87.6337V78.2888Z" fill="#323F4B"/>
|
||||
<path d="M38.6564 89.8195C37.7935 89.8195 37.1909 89.6791 36.8485 89.3984C36.506 89.1043 36.3348 88.5428 36.3348 87.7139V77.5267C36.3348 76.6578 36.506 76.0495 36.8485 75.7019C37.1909 75.3543 37.8072 75.1805 38.6975 75.1805H41.4916C42.8476 75.1805 43.9981 75.4545 44.9432 76.0027C46.0937 76.6578 46.6689 77.6003 46.6689 78.8302C46.6689 80.4211 46.08 81.4973 44.9021 82.0588V82.119C45.7787 82.3864 46.4087 82.7807 46.7922 83.3021C47.1757 83.8102 47.3675 84.512 47.3675 85.4078C47.3675 86.8917 46.8401 88.0214 45.7855 88.7968C44.8541 89.4786 43.6009 89.8195 42.0258 89.8195H38.6564ZM39.7453 83.5027V86.9519H41.4505C42.1764 86.9519 42.7175 86.8583 43.0736 86.6711C43.5393 86.4171 43.7721 85.9626 43.7721 85.3075C43.7721 84.5455 43.5461 84.0374 43.0941 83.7834C42.7654 83.5963 42.197 83.5027 41.3889 83.5027H39.7453ZM39.7453 78.0281V80.8957H41.2245C41.8957 80.8957 42.3682 80.8155 42.6421 80.6551C43.0256 80.4412 43.2174 80.0401 43.2174 79.4519C43.2174 78.5027 42.5531 78.0281 41.2245 78.0281H39.7453Z" fill="#323F4B"/>
|
||||
<path d="M64.1493 80.8957L57.9653 88.877C57.39 89.6257 56.7326 90 55.993 90C54.9657 90 54.4521 89.3382 54.4521 88.0147V77.3663C54.4521 75.7888 55.0205 75 56.1573 75C57.2941 75 57.8626 75.7888 57.8626 77.3663V84.2246L64.1698 76.0829C64.7314 75.361 65.3341 75 65.9778 75C67.0324 75 67.5598 75.7487 67.5598 77.246V87.6337C67.5598 89.2112 66.9913 90 65.8545 90C64.7177 90 64.1493 89.2112 64.1493 87.6337V80.8957Z" fill="#323F4B"/>
|
||||
<path d="M74.6391 82.5201C74.6391 80.3275 75.2691 78.5428 76.5292 77.1658C77.8304 75.7219 79.5904 75 81.8093 75C84.0418 75 85.8087 75.7152 87.1099 77.1457C88.37 78.5227 89 80.3142 89 82.5201C89 84.7126 88.37 86.4973 87.1099 87.8743C85.795 89.2914 84.0281 90 81.8093 90C79.5904 90 77.8304 89.2848 76.5292 87.8543C75.2691 86.4906 74.6391 84.7126 74.6391 82.5201ZM78.255 82.5602C78.255 83.7634 78.5358 84.7594 79.0973 85.5481C79.7274 86.4706 80.6314 86.9318 81.8093 86.9318C82.9872 86.9318 83.898 86.4773 84.5417 85.5682C85.1033 84.766 85.3841 83.7634 85.3841 82.5602C85.3841 81.3168 85.117 80.2941 84.5828 79.492C83.9391 78.5428 83.0146 78.0682 81.8093 78.0682C80.604 78.0682 79.6863 78.5428 79.0563 79.492C78.5221 80.2941 78.255 81.3168 78.255 82.5602Z" fill="#323F4B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
3
assets/icons/sucess.svg
Normal file
3
assets/icons/sucess.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.33333 16C3.85185 16 2.59259 15.4815 1.55556 14.4444C0.518519 13.4074 0 12.1481 0 10.6667V5.33333C0 3.85185 0.518519 2.59259 1.55556 1.55556C2.59259 0.518519 3.85185 0 5.33333 0H10.6667C12.1481 0 13.4074 0.518519 14.4444 1.55556C15.4815 2.59259 16 3.85185 16 5.33333V10.6667C16 12.1481 15.4815 13.4074 14.4444 14.4444C13.4074 15.4815 12.1481 16 10.6667 16H5.33333ZM7.11111 9.06667L5.77778 7.73333C5.61481 7.57037 5.40741 7.48889 5.15556 7.48889C4.9037 7.48889 4.6963 7.57037 4.53333 7.73333C4.37037 7.8963 4.28889 8.1037 4.28889 8.35556C4.28889 8.60741 4.37037 8.81481 4.53333 8.97778L6.48889 10.9333C6.66667 11.1111 6.87407 11.2 7.11111 11.2C7.34815 11.2 7.55556 11.1111 7.73333 10.9333L11.8222 6.84444C11.9852 6.68148 12.0667 6.47407 12.0667 6.22222C12.0667 5.97037 11.9852 5.76296 11.8222 5.6C11.6593 5.43704 11.4519 5.35556 11.2 5.35556C10.9481 5.35556 10.7407 5.43704 10.5778 5.6L7.11111 9.06667ZM5.33333 14.2222H10.6667C11.6444 14.2222 12.4815 13.8741 13.1778 13.1778C13.8741 12.4815 14.2222 11.6444 14.2222 10.6667V5.33333C14.2222 4.35556 13.8741 3.51852 13.1778 2.82222C12.4815 2.12593 11.6444 1.77778 10.6667 1.77778H5.33333C4.35556 1.77778 3.51852 2.12593 2.82222 2.82222C2.12593 3.51852 1.77778 4.35556 1.77778 5.33333V10.6667C1.77778 11.6444 2.12593 12.4815 2.82222 13.1778C3.51852 13.8741 4.35556 14.2222 5.33333 14.2222Z" fill="#16A249"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/logo_bg.png
Normal file
BIN
assets/images/logo_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
28593
coverage/lcov.info
28593
coverage/lcov.info
File diff suppressed because it is too large
Load Diff
@@ -388,7 +388,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -406,7 +406,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -539,7 +539,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -557,7 +557,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
@@ -578,7 +578,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 81;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -596,7 +596,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
MARKETING_VERSION = 1.2.3;
|
||||
MARKETING_VERSION = 1.3.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>81</string>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -48,8 +48,33 @@ platform :ios do
|
||||
UI.message "Configuring Flutter for release mode..."
|
||||
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)
|
||||
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(
|
||||
workspace: "Runner.xcworkspace",
|
||||
@@ -74,7 +99,7 @@ platform :ios do
|
||||
skip_submission: true,
|
||||
distribute_external: false,
|
||||
notify_external_testers: false,
|
||||
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}",
|
||||
changelog: "TestFlight build #{flutter_build_number}",
|
||||
verbose: true
|
||||
)
|
||||
rescue => ex
|
||||
|
||||
502
lib/common/widgets/markdown_editor.dart
Normal file
502
lib/common/widgets/markdown_editor.dart
Normal file
@@ -0,0 +1,502 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
|
||||
|
||||
class MarkdownEditor extends StatefulWidget {
|
||||
const MarkdownEditor({
|
||||
super.key,
|
||||
this.initialText,
|
||||
this.hintText,
|
||||
this.hasError = false,
|
||||
this.minLines = 4,
|
||||
this.maxLines = 6,
|
||||
this.heightPreview = 120,
|
||||
this.onChanged,
|
||||
this.onToolbarVisibilityChanged,
|
||||
this.showInlineToolbar = true,
|
||||
});
|
||||
|
||||
final String? initialText;
|
||||
final String? hintText;
|
||||
final bool hasError;
|
||||
final int minLines;
|
||||
final int maxLines;
|
||||
final double heightPreview;
|
||||
final void Function(String value)? onChanged;
|
||||
final void Function(bool visible)? onToolbarVisibilityChanged;
|
||||
final bool showInlineToolbar;
|
||||
|
||||
@override
|
||||
State<MarkdownEditor> createState() => MarkdownEditorState();
|
||||
}
|
||||
|
||||
class MarkdownEditorState extends State<MarkdownEditor> {
|
||||
late final TextEditingController _controller;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
bool _isPreview = false;
|
||||
bool _enablePanel = false;
|
||||
late final KeyboardVisibilityController _keyboardVisibilityController;
|
||||
late final StreamSubscription<bool> _keyboardSubscription;
|
||||
|
||||
bool get isToolbarVisible => _enablePanel || _isPreview;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialText ?? '');
|
||||
_keyboardVisibilityController = KeyboardVisibilityController();
|
||||
_keyboardSubscription = _keyboardVisibilityController.onChange.listen((visible) {
|
||||
setState(() {
|
||||
_enablePanel = visible;
|
||||
});
|
||||
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_keyboardSubscription.cancel();
|
||||
_focusNode.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _hasFormatting(String prefix, String suffix) {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
return selectedText.startsWith(prefix) && selectedText.endsWith(suffix);
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
|
||||
final beforeCursor = text.substring(cursorPosition - prefix.length, cursorPosition);
|
||||
final afterCursor = text.substring(cursorPosition, cursorPosition + suffix.length);
|
||||
return beforeCursor == prefix && afterCursor == suffix;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _removeFormatting(String prefix, String suffix) {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);
|
||||
final newText = text.replaceRange(selection.start, selection.end, unformattedText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + unformattedText.length);
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
|
||||
final beforeCursor = text.substring(0, cursorPosition - prefix.length);
|
||||
final afterCursor = text.substring(cursorPosition + suffix.length);
|
||||
final newText = beforeCursor + afterCursor;
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPosition - prefix.length);
|
||||
}
|
||||
}
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _applyMarkdownFormat(String prefix, String suffix) {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final formattedText = '$prefix$selectedText$suffix';
|
||||
final newText = text.replaceRange(selection.start, selection.end, formattedText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + formattedText.length);
|
||||
} else {
|
||||
final cursorPosition = selection.baseOffset;
|
||||
final newText = '${text.substring(0, cursorPosition)}$prefix$suffix${text.substring(cursorPosition)}';
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPosition + prefix.length);
|
||||
}
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _applyHeading() {
|
||||
if (_hasFormatting('# ', '')) {
|
||||
_removeFormatting('# ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('# ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBold() {
|
||||
if (_hasFormatting('**', '**')) {
|
||||
_removeFormatting('**', '**');
|
||||
} else {
|
||||
_applyMarkdownFormat('**', '**');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyItalic() {
|
||||
if (_hasFormatting('*', '*')) {
|
||||
_removeFormatting('*', '*');
|
||||
} else {
|
||||
_applyMarkdownFormat('*', '*');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyNumberedList() {
|
||||
if (_hasFormatting('1. ', '')) {
|
||||
_removeFormatting('1. ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('1. ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyBulletedList() {
|
||||
if (_hasFormatting('- ', '')) {
|
||||
_removeFormatting('- ', '');
|
||||
} else {
|
||||
_applyMarkdownFormat('- ', '');
|
||||
}
|
||||
}
|
||||
|
||||
void _applyLink() {
|
||||
final text = _controller.text;
|
||||
final selection = _controller.selection;
|
||||
if (selection.isValid && !selection.isCollapsed) {
|
||||
final selectedText = text.substring(selection.start, selection.end);
|
||||
final linkRegex = RegExp(r'^\[(.*?)\]\((.*?)\)$');
|
||||
if (linkRegex.hasMatch(selectedText)) {
|
||||
final match = linkRegex.firstMatch(selectedText);
|
||||
final linkText = match?.group(1) ?? selectedText;
|
||||
final newText = text.replaceRange(selection.start, selection.end, linkText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + linkText.length);
|
||||
} else {
|
||||
final formattedText = '[$selectedText](url)';
|
||||
final newText = text.replaceRange(selection.start, selection.end, formattedText);
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection(baseOffset: selection.start + selectedText.length + 2, extentOffset: selection.start + selectedText.length + 5);
|
||||
}
|
||||
} else {
|
||||
_applyMarkdownFormat('[', '](url)');
|
||||
}
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _applyHorizontalRule() {
|
||||
final cursorPosition = _controller.selection.baseOffset;
|
||||
final text = _controller.text;
|
||||
final newText = '${text.substring(0, cursorPosition)}\n---\n${text.substring(cursorPosition)}';
|
||||
_controller.text = newText;
|
||||
_controller.selection = TextSelection.collapsed(offset: cursorPosition + 5);
|
||||
widget.onChanged?.call(_controller.text);
|
||||
}
|
||||
|
||||
void _togglePreview() {
|
||||
setState(() {
|
||||
_isPreview = !_isPreview;
|
||||
});
|
||||
if (!_isPreview) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
} else {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
|
||||
}
|
||||
|
||||
Widget _mdButton(IconData icon, VoidCallback onTap) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
|
||||
child: Icon(icon),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mdTextButton(String text, VoidCallback onTap) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.black87),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMarkdownPreview(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.heightPreview,
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF6F6F6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFFE0E0E0)),
|
||||
),
|
||||
child: SingleChildScrollView(child: _parseMarkdown(_controller.text)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildToolbar(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width - 76,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
_mdTextButton('H', _applyHeading),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_bold, _applyBold),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_italic, _applyItalic),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_numbered, _applyNumberedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.link, _applyLink),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
|
||||
),
|
||||
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _parseMarkdown(String text) {
|
||||
if (text.isEmpty) {
|
||||
return Text(widget.hintText ?? '');
|
||||
}
|
||||
final lines = text.split('\n');
|
||||
final widgets = <Widget>[];
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final line = lines[i];
|
||||
if (line.trim().isEmpty) {
|
||||
widgets.add(const SizedBox(height: 8));
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('# ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(2),
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('## ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(3),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('### ')) {
|
||||
widgets.add(
|
||||
Text(
|
||||
line.substring(4),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (line.startsWith('- ') || line.startsWith('* ')) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('• ', style: TextStyle(fontSize: 16)),
|
||||
Expanded(child: _parseInlineMarkdown(line.substring(2))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (RegExp(r'^\d+\. ').hasMatch(line)) {
|
||||
final match = RegExp(r'^(\d+)\. ').firstMatch(line);
|
||||
final number = match?.group(1) ?? '1';
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$number. ', style: const TextStyle(fontSize: 16)),
|
||||
Expanded(child: _parseInlineMarkdown(line.substring(number.length + 2))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (line.trim() == '---') {
|
||||
widgets.add(const Divider(color: Colors.grey, thickness: 1));
|
||||
} else {
|
||||
widgets.add(_parseInlineMarkdown(line));
|
||||
}
|
||||
if (i < lines.length - 1) {
|
||||
widgets.add(const SizedBox(height: 4));
|
||||
}
|
||||
}
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: widgets);
|
||||
}
|
||||
|
||||
Widget _parseInlineMarkdown(String text) {
|
||||
final spans = <TextSpan>[];
|
||||
final regex = RegExp(r'(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))');
|
||||
int lastEnd = 0;
|
||||
for (final match in regex.allMatches(text)) {
|
||||
if (match.start > lastEnd) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text.substring(lastEnd, match.start),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}
|
||||
final matchedText = match.group(0)!;
|
||||
if (matchedText.startsWith('**') && matchedText.endsWith('**')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: matchedText.substring(2, matchedText.length - 2),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (matchedText.startsWith('*') && matchedText.endsWith('*')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: matchedText.substring(1, matchedText.length - 1),
|
||||
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.black),
|
||||
),
|
||||
);
|
||||
} else if (matchedText.startsWith('[') && matchedText.contains('](')) {
|
||||
final linkMatch = RegExp(r'\[(.*?)\]\((.*?)\)').firstMatch(matchedText);
|
||||
if (linkMatch != null) {
|
||||
final linkText = linkMatch.group(1)!;
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: linkText,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.blue, decoration: TextDecoration.underline),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
lastEnd = match.end;
|
||||
}
|
||||
if (lastEnd < text.length) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: text.substring(lastEnd),
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}
|
||||
return RichText(text: TextSpan(children: spans));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_isPreview)
|
||||
_buildMarkdownPreview(context)
|
||||
else
|
||||
CustomTextFormField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
hintText: widget.hintText,
|
||||
hasError: widget.hasError,
|
||||
onChanged: (value) => widget.onChanged?.call(value),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.showInlineToolbar && (isToolbarVisible || !_isPreview))
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
_mdTextButton('H', _applyHeading),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_bold, _applyBold),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_italic, _applyItalic),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_numbered, _applyNumberedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.link, _applyLink),
|
||||
const SizedBox(width: 8),
|
||||
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
|
||||
],
|
||||
),
|
||||
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
@@ -32,14 +33,16 @@ class AppConfig {
|
||||
}
|
||||
|
||||
/// Список глобальных BlocProvider'ов
|
||||
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
|
||||
List<BlocProvider> get globalBlocProviders => [
|
||||
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()),
|
||||
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()),
|
||||
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()),
|
||||
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()),
|
||||
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
|
||||
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
|
||||
BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
|
||||
BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
|
||||
BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
|
||||
BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
|
||||
BlocProvider<ChatsCubit>(lazy: true, create: (context) => sl<ChatsCubit>()),
|
||||
BlocProvider<ViewsCubit>(lazy: true, create: (context) => sl<ViewsCubit>()),
|
||||
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
|
||||
BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
|
||||
];
|
||||
|
||||
/// Делегаты локализации
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io' as io;
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
@@ -37,9 +39,19 @@ class AppInitializer {
|
||||
|
||||
/// Инициализация Firebase
|
||||
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) {
|
||||
await FirebaseMessaging.instance.requestPermission();
|
||||
try {
|
||||
await FirebaseMessaging.instance.requestPermission().timeout(const Duration(seconds: 5));
|
||||
} catch (e) {
|
||||
// Разрешение не критично, продолжаем работу
|
||||
log('Failed to request Firebase messaging permission: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +80,10 @@ class AppInitializer {
|
||||
options.tracesSampleRate = 1.0;
|
||||
options.debug = false;
|
||||
}, appRunner: appRunner);
|
||||
FlutterError.onError = (details) async {
|
||||
if (kDebugMode) return;
|
||||
await Sentry.captureException(details.exception, stackTrace: details.stack);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:eatvio_app/core/routing/auth_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/content_routes.dart';
|
||||
import 'package:eatvio_app/core/routing/main_navigation_routes.dart';
|
||||
@@ -13,6 +14,9 @@ import 'package:go_router/go_router.dart';
|
||||
class AppRouter {
|
||||
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
// Домены для deep links (в т.ч. ссылки из пушей dev2.eatvio.com)
|
||||
static const List<String> _deepLinkHosts = ['eatvio.ru', 'www.eatvio.ru', 'dev2.eatvio.com', 'www.dev2.eatvio.com'];
|
||||
|
||||
static GoRouter appRouter = GoRouter(
|
||||
initialLocation: RouteConstants.updateScreen,
|
||||
navigatorKey: navigatorKey,
|
||||
@@ -26,52 +30,180 @@ class AppRouter {
|
||||
],
|
||||
);
|
||||
|
||||
/// Обработка редиректов для deep links
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
final segments = state.uri.pathSegments;
|
||||
static String? _resolveDeepLinkUri(Uri uri) {
|
||||
final host = uri.host.toLowerCase();
|
||||
final segments = uri.pathSegments;
|
||||
|
||||
final isDeepLink = _deepLinkHosts.contains(host);
|
||||
|
||||
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');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
if (segments.length >= 4 &&
|
||||
segments[0] == 'api' &&
|
||||
segments[1] == 'v2' &&
|
||||
segments[2] == 'chat' &&
|
||||
segments[3] == 'show') {
|
||||
developer.log('Deep link: api/v2/chat/show → chat-select-screen', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
}
|
||||
|
||||
if (segments[0] != 'ru' && segments[0] != 'en') {
|
||||
developer.log('Invalid language segment: ${segments[0]}, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
if (segments.length == 4) {
|
||||
final type = segments[1];
|
||||
switch (type) {
|
||||
case 'diary':
|
||||
FlutterNativeSplash.remove();
|
||||
return '/diary-screen';
|
||||
|
||||
if (type == 'diary') {
|
||||
developer.log('Deep link: diary, redirecting to diary-screen', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
}
|
||||
}
|
||||
if (segments.length == 3) {
|
||||
if (type == 'consultations' && segments[2] == 'status') {
|
||||
final consultationId = segments[3];
|
||||
developer.log('Deep link: consultations/status/$consultationId', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '${RouteConstants.consultationsScreen}?id=$consultationId';
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link type for 4 segments: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
} else if (segments.length == 3) {
|
||||
final type = segments[1];
|
||||
final contentIdentifier = segments[2];
|
||||
|
||||
switch (type) {
|
||||
case 'recipe':
|
||||
developer.log('Deep link: recipe/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-recipes-screen?slug=$contentIdentifier';
|
||||
case 'blog_post':
|
||||
developer.log('Deep link: blog_post/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/blog-post-screen?id=$contentIdentifier';
|
||||
case 'plan':
|
||||
developer.log('Deep link: plan/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-plan-screen?slug=$contentIdentifier';
|
||||
case 'diet':
|
||||
case 'diet': {
|
||||
final dietId = int.tryParse(contentIdentifier);
|
||||
if (dietId != null) {
|
||||
developer.log('Deep link: diet/id=$dietId', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?id=$dietId';
|
||||
}
|
||||
developer.log('Deep link: diet/slug=$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/about-diet-screen?slug=$contentIdentifier';
|
||||
}
|
||||
case 'chat':
|
||||
developer.log('Deep link: chat/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/chat-select-screen';
|
||||
return RouteConstants.chatSelectScreen;
|
||||
case 'profile':
|
||||
// В диплинке передан slug — открываем профиль пользователя по slug.
|
||||
developer.log('Deep link: profile/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/profile-screen?slug=$contentIdentifier';
|
||||
}
|
||||
}
|
||||
if (segments.length == 2) {
|
||||
final type = segments[1];
|
||||
switch (type) {
|
||||
case 'chat':
|
||||
case 'badge':
|
||||
// В badge нет slug, открываем вкладку «Мой профиль».
|
||||
developer.log('Deep link: badge → my-profile tab', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return '/chat-select-screen';
|
||||
return RouteConstants.myProfileScreen;
|
||||
case 'diary':
|
||||
developer.log('Deep link: diary/$contentIdentifier', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
case 'consultant_diary_comment':
|
||||
developer.log('Deep link: consultant_diary_comment/$contentIdentifier → diary', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
default:
|
||||
developer.log('Unknown deep link type: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
} else if (segments.length == 2) {
|
||||
final type = segments[1];
|
||||
|
||||
if (type == 'chat') {
|
||||
developer.log('Deep link: chat', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.chatSelectScreen;
|
||||
}
|
||||
if (type == 'diary') {
|
||||
developer.log('Deep link: diary', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.diaryScreen;
|
||||
}
|
||||
|
||||
developer.log('Unknown deep link type for 2 segments: $type, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
return null;
|
||||
developer.log('Unknown deep link format: ${segments.length} segments, redirecting to feed', name: 'AppRouter');
|
||||
FlutterNativeSplash.remove();
|
||||
return RouteConstants.feedScreen;
|
||||
}
|
||||
|
||||
static String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||
return _resolveDeepLinkUri(state.uri);
|
||||
}
|
||||
|
||||
static void navigateByDeepLinkUrl(String url) {
|
||||
if (url.isEmpty) {
|
||||
developer.log('navigateByDeepLinkUrl: empty url, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri? uri;
|
||||
try {
|
||||
uri = Uri.tryParse(url);
|
||||
} catch (e) {
|
||||
developer.log('navigateByDeepLinkUrl: failed to parse url=$url, error=$e, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri == null) {
|
||||
developer.log('navigateByDeepLinkUrl: uri is null for url=$url, redirecting to feed', name: 'AppRouter');
|
||||
appRouter.go(RouteConstants.feedScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uri.host.isEmpty && uri.pathSegments.isNotEmpty) {
|
||||
uri = Uri(
|
||||
scheme: 'https',
|
||||
host: _deepLinkHosts.first,
|
||||
pathSegments: uri.pathSegments,
|
||||
queryParameters: uri.queryParameters.isEmpty ? null : uri.queryParameters,
|
||||
);
|
||||
}
|
||||
|
||||
final target = _resolveDeepLinkUri(uri);
|
||||
|
||||
if (target != null) {
|
||||
appRouter.go(target);
|
||||
} else {
|
||||
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}) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/core/comments/models/comment_update_model.dart';
|
||||
import 'package:eatvio_app/core/comments/services/comments_service.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class CommentsRepository {
|
||||
final CommentsService service;
|
||||
@@ -9,7 +10,8 @@ class CommentsRepository {
|
||||
Future<void> deleteComment({required int id}) async {
|
||||
try {
|
||||
return service.deleteComment(id);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +19,8 @@ class CommentsRepository {
|
||||
Future<void> updateComment({required int id, required String body}) async {
|
||||
try {
|
||||
return service.updateComment(id, CommentUpdateModel(body: body));
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,4 +69,9 @@ class AppConstants {
|
||||
static const String usersEndpoint = '/users';
|
||||
static const String postsEndpoint = '/posts';
|
||||
static const String commentsEndpoint = '/comments';
|
||||
|
||||
// Delay of pending requests
|
||||
/// 30 секунд стоит потому что запрос может идти 12-15 секунд
|
||||
/// Особенно если производится отладка на сервере разработчика
|
||||
static const Duration pendingDelay = Duration(seconds: 30);
|
||||
}
|
||||
|
||||
29
lib/core/cubit/marketing/marketing_cubit.dart
Normal file
29
lib/core/cubit/marketing/marketing_cubit.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart';
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MarketingCubit extends Cubit<MarketingCubitState> {
|
||||
final Prefs prefs;
|
||||
MarketingCubit(this.prefs) : super(const MarketingCubitState());
|
||||
|
||||
Future<bool> checkRegistrationDate() async {
|
||||
try {
|
||||
final milliseconds = prefs.getInt(PrefsKeys.registrationDate);
|
||||
final marketingBottomSheetHasBeenShown = prefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown);
|
||||
if (milliseconds == null || marketingBottomSheetHasBeenShown == true) {
|
||||
return false;
|
||||
}
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(milliseconds);
|
||||
final today = DateTime.now();
|
||||
final daysCount = today.difference(date).inDays;
|
||||
final needToShowMarketingBottomSheet = daysCount >= 14;
|
||||
if (needToShowMarketingBottomSheet) {
|
||||
await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true);
|
||||
}
|
||||
return needToShowMarketingBottomSheet;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
lib/core/cubit/marketing/marketing_cubit_state.dart
Normal file
3
lib/core/cubit/marketing/marketing_cubit_state.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class MarketingCubitState {
|
||||
const MarketingCubitState();
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class StateUtils {
|
||||
if (isError(state)) {
|
||||
// Попытка извлечь сообщение об ошибке из состояния
|
||||
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 null;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
|
||||
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||
@@ -38,14 +39,16 @@ import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cub
|
||||
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||
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/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/verify_email_cubit/verify_email_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
|
||||
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
|
||||
@@ -73,11 +76,12 @@ class CubitRegistrations {
|
||||
/// Регистрация Cubit'ов как фабрик
|
||||
static void _registerFactoryCubits() {
|
||||
// Auth related cubits
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
|
||||
// Feed related cubits
|
||||
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
|
||||
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
|
||||
@@ -110,7 +114,7 @@ class CubitRegistrations {
|
||||
// User content cubits
|
||||
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: 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
|
||||
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
|
||||
@@ -143,5 +147,7 @@ class CubitRegistrations {
|
||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
|
||||
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
|
||||
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
|
||||
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/constants/app_constants.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/prefs.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sentry_dio/sentry_dio.dart';
|
||||
|
||||
/// Регистрация внешних зависимостей в системе внедрения зависимостей
|
||||
class ExternalRegistrations {
|
||||
@@ -19,14 +21,21 @@ class ExternalRegistrations {
|
||||
/// Регистрация Dio HTTP клиента
|
||||
static void _registerDio() {
|
||||
sl.registerLazySingleton(
|
||||
() =>
|
||||
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
||||
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
||||
() => Dio(
|
||||
BaseOptions(
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Регистрация Prefs
|
||||
static void _registerPrefs() {
|
||||
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance()));
|
||||
sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,18 +17,21 @@ 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/feed/repositories/feed_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/repositories/notification_settings_repository.dart';
|
||||
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
|
||||
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
|
||||
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
|
||||
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
|
||||
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
|
||||
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
|
||||
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
|
||||
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
@@ -53,6 +56,7 @@ class RepositoryRegistrations {
|
||||
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
|
||||
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
|
||||
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
|
||||
sl.registerLazySingleton<BarcodeRepository>(() => BarcodeRepository(barcodeService: sl(), dio: sl()));
|
||||
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
|
||||
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
|
||||
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
|
||||
@@ -68,6 +72,8 @@ class RepositoryRegistrations {
|
||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
|
||||
sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl()));
|
||||
|
||||
// Content repositories
|
||||
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));
|
||||
|
||||
@@ -19,18 +19,21 @@ 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/feed/services/feed_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/notification_settings_service.dart';
|
||||
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
|
||||
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
|
||||
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
|
||||
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plan_service.dart';
|
||||
import 'package:eatvio_app/features/plans/services/plans_service.dart';
|
||||
import 'package:eatvio_app/features/profile/services/profile_service.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_service.dart';
|
||||
import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
|
||||
import 'package:eatvio_app/features/settings/services/settings_service.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
@@ -54,6 +57,7 @@ class ServiceRegistrations {
|
||||
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
|
||||
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
|
||||
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
|
||||
sl.registerLazySingleton<BarcodeService>(() => BarcodeService(sl()));
|
||||
sl.registerLazySingleton<DietService>(() => DietService(sl()));
|
||||
sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
|
||||
sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
|
||||
@@ -76,6 +80,8 @@ class ServiceRegistrations {
|
||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
||||
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
|
||||
sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl()));
|
||||
|
||||
// Content services
|
||||
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class FirebaseApi {
|
||||
final _firebaseMessaging = FirebaseMessaging.instance;
|
||||
@@ -19,12 +20,25 @@ class FirebaseApi {
|
||||
String? token;
|
||||
|
||||
if (await _isHuaweiDevice()) {
|
||||
token = await _getHuaweiPushTokenNative();
|
||||
token = await _getHuaweiPushTokenNative().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
@@ -35,13 +49,20 @@ class FirebaseApi {
|
||||
await sl<Dio>().post(
|
||||
'/api/anonymous_token',
|
||||
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,
|
||||
'token': await getToken(),
|
||||
},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
throw TimeoutException('registerAnonymousPushToken timeout');
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +78,16 @@ class FirebaseApi {
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
throw TimeoutException('registerPushToken timeout');
|
||||
},
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
// Не пробрасываем ошибку дальше, так как это не критичная операция
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,10 +112,12 @@ class FirebaseApi {
|
||||
} on PlatformException catch (e, st) {
|
||||
showErrorToast('$e $st');
|
||||
log(e.toString(), stackTrace: st);
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
return null;
|
||||
} catch (e, st) {
|
||||
showErrorToast('$e $st');
|
||||
log(e.toString(), stackTrace: st);
|
||||
await Sentry.captureException(e, stackTrace: st);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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:image/image.dart' as img;
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -27,6 +30,17 @@ abstract class ImageCompressUtility {
|
||||
}
|
||||
|
||||
static Future<File> compressFile(File file) async {
|
||||
// Skip compression in test environment to avoid hangs
|
||||
try {
|
||||
final appConfig = sl<AppConfig>();
|
||||
if (appConfig.buildType == AppBuildType.tests) {
|
||||
// In tests, return the original file without compression
|
||||
return file;
|
||||
}
|
||||
} catch (_) {
|
||||
// If AppConfig is not available, proceed with compression
|
||||
}
|
||||
|
||||
final image = img.decodeImage(file.readAsBytesSync())!;
|
||||
int width = image.width;
|
||||
int height = image.height;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_router.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';
|
||||
|
||||
class TokenInterceptor extends Interceptor {
|
||||
@@ -23,6 +25,7 @@ class TokenInterceptor extends Interceptor {
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
if (err.response?.statusCode == 401) {
|
||||
await sl<WsManager>().clear();
|
||||
await TokenStorage.removeTokenInStorage();
|
||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
||||
}
|
||||
|
||||
@@ -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/repository_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:get_it/get_it.dart';
|
||||
|
||||
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
|
||||
// Регистрация внешних зависимостей (Dio, etc.)
|
||||
ExternalRegistrations.registerAll();
|
||||
|
||||
// Ожидание инициализации асинхронных зависимостей
|
||||
await sl.getAsync<Prefs>();
|
||||
|
||||
// Регистрация сервисов
|
||||
ServiceRegistrations.registerAll();
|
||||
|
||||
|
||||
11
lib/core/mixins/registration_date_cache_mixin.dart
Normal file
11
lib/core/mixins/registration_date_cache_mixin.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:eatvio_app/core/prefs.dart';
|
||||
import 'package:eatvio_app/core/prefs_keys.dart';
|
||||
|
||||
mixin RegistrationDateCacheMixin {
|
||||
late final Prefs prefs;
|
||||
|
||||
|
||||
Future<void> cacheRegistrationDate(DateTime date) async {
|
||||
await prefs.setInt(PrefsKeys.registrationDate, date.millisecondsSinceEpoch);
|
||||
}
|
||||
}
|
||||
91
lib/core/notifications/push_navigation_handler.dart
Normal file
91
lib/core/notifications/push_navigation_handler.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
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');
|
||||
|
||||
/// Инициализация обработчиков Firebase Messaging.
|
||||
/// Должна вызываться один раз после старта приложения.
|
||||
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'),
|
||||
);
|
||||
|
||||
// Подписка на события от native-кода (HMS через MethodChannel).
|
||||
_hmsChannel.setMethodCallHandler((call) async {
|
||||
if (call.method == 'openActionUrl') {
|
||||
final url = call.arguments as String? ?? '';
|
||||
handleActionUrlFromNative(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Входная точка для навигации из native-кода (например, HMS через MethodChannel).
|
||||
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 }
|
||||
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/features/auth/entities/auth_result_entity.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||
@@ -9,6 +7,7 @@ import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
|
||||
import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Маршруты для аутентификации
|
||||
@@ -20,12 +19,11 @@ class AuthRoutes {
|
||||
path: RouteConstants.authScreen,
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
redirect: (context, state) {
|
||||
sl<FirebaseApi>().registerAnonymousPushToken();
|
||||
return null;
|
||||
},
|
||||
// registerAnonymousPushToken вызывается в AuthCubit.initializeUser()
|
||||
// и не должен блокировать навигацию
|
||||
routes: [
|
||||
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.forgetPasswordScreen,
|
||||
name: 'forgetPasswordScreen',
|
||||
|
||||
@@ -17,12 +17,20 @@ class ContentRoutes {
|
||||
name: 'aboutPlanScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
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>;
|
||||
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(
|
||||
@@ -30,12 +38,20 @@ class ContentRoutes {
|
||||
name: 'aboutRecipesScreen',
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
|
||||
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>;
|
||||
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(
|
||||
@@ -43,16 +59,28 @@ class ContentRoutes {
|
||||
name: 'aboutDietScreen',
|
||||
builder: (context, state) {
|
||||
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) {
|
||||
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>;
|
||||
return AboutDietScreen(
|
||||
id: data['id'] as int?,
|
||||
slug: data['slug'] as String?,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool,
|
||||
);
|
||||
if (id != null && id > 0) {
|
||||
return AboutDietScreen(id: id, slug: null, index: null, navigateToComments: commentId != null, highlightedCommentId: commentId);
|
||||
}
|
||||
final data = state.extra as Map<String, dynamic>?;
|
||||
if (data != null) {
|
||||
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(
|
||||
@@ -65,12 +93,20 @@ class ContentRoutes {
|
||||
name: 'blogPostScreen',
|
||||
builder: (context, state) {
|
||||
final id = state.uri.queryParameters['id'];
|
||||
final commentIdStr = state.uri.queryParameters['commentId'];
|
||||
final commentId = int.tryParse(commentIdStr ?? '');
|
||||
final parsedId = int.tryParse(id ?? '');
|
||||
|
||||
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/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/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/chat/presentation/pages/chats_screen.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
|
||||
@@ -23,13 +24,18 @@ class MainNavigationRoutes {
|
||||
return [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) {
|
||||
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
|
||||
builder: (BuildContext context, ProfileAvatarState state) {
|
||||
if (state is ProfileAvatarStateLoaded) {
|
||||
return BottomNavigationBarWidget(profileModel: state.profile, child: child);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
// Инициализация ProfileAvatarCubit при первом использовании ShellRoute
|
||||
final profileAvatarCubit = context.read<ProfileAvatarCubit>();
|
||||
if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
|
||||
// Вызываем getMyProfile() только если состояние loading (первый запуск)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
profileAvatarCubit.getMyProfile();
|
||||
});
|
||||
}
|
||||
|
||||
return _MainShellWithNavScope(
|
||||
currentLocation: state.matchedLocation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
routes: [
|
||||
@@ -56,7 +62,7 @@ class MainNavigationRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.diaryScreen,
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
|
||||
pageBuilder: (context, state) => NoTransitionPage(child: DiaryScreen(date: state.extra as DateTime?)),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
@@ -66,7 +72,10 @@ class MainNavigationRoutes {
|
||||
GoRoute(
|
||||
path: RouteConstants.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(
|
||||
path: RouteConstants.notificationsScreen,
|
||||
@@ -92,3 +101,33 @@ class MainNavigationRoutes {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// 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:eatvio_app/core/app_config.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/routing/route_constants.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/chat/presentation/pages/chat_screen.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
|
||||
@@ -29,6 +31,7 @@ import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
|
||||
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/models/profile_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
|
||||
@@ -57,24 +60,33 @@ class OtherRoutes {
|
||||
builder: (context, state) => const WelcomeScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
final prefs = sl<Prefs>();
|
||||
final firebaseApi = sl<FirebaseApi>();
|
||||
// Prefs уже инициализирован в locator_service, но на всякий случай проверяем
|
||||
final prefs = await sl.getAsync<Prefs>();
|
||||
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
|
||||
await firebaseApi.registerPushToken();
|
||||
if (!welcomeScreenWasShowed) {
|
||||
return null;
|
||||
} else if (await TokenStorage.haveTokenInStorage()) {
|
||||
return '/feed-screen';
|
||||
} else {
|
||||
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
||||
if (!hasAnonymousTokenBeenSent) {
|
||||
await firebaseApi.registerAnonymousPushToken();
|
||||
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
||||
}
|
||||
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
try {
|
||||
final profileRepository = sl<ProfileRepository>();
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
|
||||
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';
|
||||
}
|
||||
} catch (e) {
|
||||
return '/feed-screen';
|
||||
return '/auth-screen';
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -84,7 +96,27 @@ class OtherRoutes {
|
||||
builder: (context, state) => const UpdateScreen(),
|
||||
redirect: (context, state) async {
|
||||
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();
|
||||
String version = packageInfo.version;
|
||||
Version currentVersion = Version.parse(version);
|
||||
@@ -95,6 +127,7 @@ class OtherRoutes {
|
||||
return RouteConstants.welcomeScreen;
|
||||
}
|
||||
} catch (e) {
|
||||
// При любой ошибке пропускаем экран обновления
|
||||
return RouteConstants.welcomeScreen;
|
||||
}
|
||||
},
|
||||
@@ -120,22 +153,35 @@ class OtherRoutes {
|
||||
GoRoute(
|
||||
path: RecipeCreateScreen.path,
|
||||
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(
|
||||
path: CreateDietScreen.path,
|
||||
name: CreateDietScreen.name,
|
||||
builder: (context, state) {
|
||||
final diet = state.extra as Diet?;
|
||||
return CreateDietScreen(diet: diet);
|
||||
final data = state.extra as Map<String, dynamic>?;
|
||||
final diet = data?['diet'] as Diet?;
|
||||
final isEditing = data?['isEditing'] as bool?;
|
||||
return CreateDietScreen(diet: diet, isEditing: isEditing);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: DietPreviewScreen.path,
|
||||
name: DietPreviewScreen.name,
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Diet;
|
||||
return DietPreviewScreen(diet: data);
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
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()),
|
||||
@@ -156,9 +202,9 @@ class OtherRoutes {
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteConstants.subsScreen,
|
||||
path: '${RouteConstants.subsScreen}/:slug',
|
||||
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
|
||||
@@ -194,7 +240,11 @@ class OtherRoutes {
|
||||
),
|
||||
|
||||
// Other routes
|
||||
GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String)),
|
||||
GoRoute(
|
||||
path: RouteConstants.glossaryScreen,
|
||||
name: 'glossaryScreen',
|
||||
builder: (context, state) => GlossaryScreen(passKey: state.extra as String),
|
||||
),
|
||||
GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
|
||||
GoRoute(
|
||||
path: RouteConstants.notificationsSettingsScreen,
|
||||
@@ -235,10 +285,14 @@ class OtherRoutes {
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, int>;
|
||||
final diaryId = data['diaryId'] as int;
|
||||
final currentPlanDay = data['currentDiaryDay'] as int;
|
||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
return UserDiaryScreen(diaryId: diaryId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: PersonalDietSuggestionScreen.path,
|
||||
name: PersonalDietSuggestionScreen.name,
|
||||
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ class RouteConstants {
|
||||
// Auth routes
|
||||
static const String authScreen = '/auth-screen';
|
||||
static const String registrationScreen = 'registration-screen';
|
||||
static const String registrationScreenV2 = 'registration-screen-v2';
|
||||
static const String forgetPasswordScreen = 'forget-password-screen';
|
||||
static const String scanQRCodeScreen = 'scan-qr-code-screen';
|
||||
static const String verifyScreen = '/verify-screen';
|
||||
|
||||
@@ -10,161 +10,76 @@ class CustomTextStyles {
|
||||
static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
|
||||
|
||||
// Title text style
|
||||
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(fontWeight: FontWeight.w600);
|
||||
|
||||
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
|
||||
color: appTheme.black900,
|
||||
);
|
||||
static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900);
|
||||
|
||||
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
|
||||
color: appTheme.black900,
|
||||
fontSize: 13,
|
||||
);
|
||||
static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900, fontSize: 13);
|
||||
|
||||
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
|
||||
color: appTheme.black,
|
||||
);
|
||||
static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
|
||||
|
||||
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
|
||||
color: appTheme.black,
|
||||
);
|
||||
static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
|
||||
|
||||
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
|
||||
color: appTheme.black,
|
||||
);
|
||||
static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
|
||||
|
||||
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
|
||||
color: appTheme.whiteA700,
|
||||
);
|
||||
static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.whiteA700);
|
||||
|
||||
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
|
||||
color: appTheme.black900,
|
||||
fontSize: 12,
|
||||
);
|
||||
static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black900, fontSize: 12);
|
||||
|
||||
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
|
||||
color: appTheme.black,
|
||||
fontSize: 12,
|
||||
);
|
||||
static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
|
||||
|
||||
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
|
||||
color: appTheme.black,
|
||||
fontSize: 12,
|
||||
);
|
||||
static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
|
||||
|
||||
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
|
||||
color: appTheme.black,
|
||||
fontSize: 12,
|
||||
);
|
||||
static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
|
||||
|
||||
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
|
||||
color: appTheme.blueGray900,
|
||||
fontSize: 12,
|
||||
);
|
||||
static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.blueGray900, fontSize: 12);
|
||||
|
||||
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
|
||||
color: appTheme.gray600,
|
||||
);
|
||||
static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.gray600);
|
||||
|
||||
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontSize: 12,
|
||||
);
|
||||
static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(color: theme.colorScheme.primary, fontSize: 12);
|
||||
|
||||
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
|
||||
color: appTheme.black900,
|
||||
fontSize: 12,
|
||||
);
|
||||
static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(color: appTheme.black900, fontSize: 12);
|
||||
|
||||
// Label text style
|
||||
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
|
||||
color: appTheme.gray60003,
|
||||
);
|
||||
static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(color: appTheme.gray60003);
|
||||
|
||||
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
|
||||
color: appTheme.whiteA700,
|
||||
);
|
||||
static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(color: appTheme.whiteA700);
|
||||
|
||||
// Title text style
|
||||
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
);
|
||||
static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(fontSize: 16);
|
||||
|
||||
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w900,
|
||||
);
|
||||
static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(fontSize: 17, fontWeight: FontWeight.w900);
|
||||
|
||||
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
|
||||
color: appTheme.black900,
|
||||
);
|
||||
static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(color: appTheme.black900);
|
||||
|
||||
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600);
|
||||
|
||||
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
|
||||
color: appTheme.whiteA700,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(color: appTheme.whiteA700, fontWeight: FontWeight.w600);
|
||||
|
||||
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.blue800,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.blue800, fontWeight: FontWeight.w800);
|
||||
|
||||
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.black,
|
||||
);
|
||||
static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.black);
|
||||
|
||||
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.black,
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w700);
|
||||
|
||||
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.black,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w800);
|
||||
|
||||
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
);
|
||||
static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(fontSize: 15, fontWeight: FontWeight.w700);
|
||||
|
||||
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.greenA70001,
|
||||
);
|
||||
static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001);
|
||||
|
||||
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.greenA70001,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001, fontWeight: FontWeight.w800);
|
||||
|
||||
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.lightGreen300,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(color: appTheme.lightGreen300, fontWeight: FontWeight.w800);
|
||||
|
||||
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
|
||||
color: appTheme.red200,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(color: appTheme.red200, fontWeight: FontWeight.w800);
|
||||
|
||||
static TextStyle get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
static TextStyle get titleSmallSecondaryContainer =>
|
||||
theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer, fontWeight: FontWeight.w800);
|
||||
|
||||
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
);
|
||||
static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer);
|
||||
|
||||
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
);
|
||||
static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(color: theme.colorScheme.onPrimary);
|
||||
}
|
||||
|
||||
extension on TextStyle {
|
||||
@@ -178,75 +93,32 @@ extension on TextStyle {
|
||||
}
|
||||
|
||||
abstract class TextStyles {
|
||||
static const headingXLSemibold = TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.255,
|
||||
);
|
||||
static const headingXLSemibold = TextStyle(fontSize: 20, fontWeight: FontWeight.w600, height: 1.255);
|
||||
|
||||
static const headingFS40FW600 = TextStyle(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.255,
|
||||
);
|
||||
static const headingFS40FW600 = TextStyle(fontSize: 40, fontWeight: FontWeight.w600, height: 1.255);
|
||||
|
||||
static const textLSemibold = TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.255,
|
||||
);
|
||||
static const textLSemibold = TextStyle(fontSize: 18, fontWeight: FontWeight.w600, height: 1.255);
|
||||
|
||||
static const textLMedium = TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.255,
|
||||
);
|
||||
|
||||
static const textMSemibold = TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.255,
|
||||
);
|
||||
static const textLMedium = TextStyle(fontSize: 18, fontWeight: FontWeight.w500, height: 1.255);
|
||||
|
||||
static const textMMedium = TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
);
|
||||
static const textMSemibold = TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1.255);
|
||||
|
||||
static const textMMediumAuto = TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.255,
|
||||
);
|
||||
static const textMMedium = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1);
|
||||
|
||||
static const textSSemibold = TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.255,
|
||||
);
|
||||
static const textMMediumAuto = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.255);
|
||||
|
||||
static const textSMedium = TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.255,
|
||||
);
|
||||
static const textSSemibold = TextStyle(fontSize: 14, fontWeight: FontWeight.w600, height: 1.255);
|
||||
|
||||
static const textFS13FW400 = TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.255,
|
||||
);
|
||||
static const textSMedium = TextStyle(fontSize: 14, fontWeight: FontWeight.w500, height: 1.255);
|
||||
|
||||
static const textXSSemibold = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.255,
|
||||
);
|
||||
static const textFS13FW400 = TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1.255);
|
||||
|
||||
static const textXSSemibold = TextStyle(fontSize: 12, fontWeight: FontWeight.w500, height: 1.255);
|
||||
|
||||
static const textFS10FW500 = TextStyle(fontSize: 10, fontWeight: FontWeight.w500, height: 1.255);
|
||||
|
||||
static const textFS24FW700 = TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.255);
|
||||
|
||||
static const textFS22FW600 = TextStyle(fontSize: 22, fontWeight: FontWeight.w600, height: 1.255);
|
||||
|
||||
static const textFS10FW500 = TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.255,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
|
||||
/// Helper class for managing themes and colors.
|
||||
class ThemeHelper {
|
||||
// The current app theme
|
||||
final _appTheme = 'primary';
|
||||
|
||||
// A map of custom color themes supported by the app
|
||||
final Map<String, PrimaryColors> _supportedCustomColor = {
|
||||
'primary': PrimaryColors(),
|
||||
};
|
||||
// A map of custom color themes supported by the app
|
||||
final Map<String, PrimaryColors> _supportedCustomColor = {'primary': PrimaryColors()};
|
||||
|
||||
// A map of color schemes supported by the app
|
||||
final Map<String, ColorScheme> _supportedColorScheme = {
|
||||
'primary': ColorSchemes.primaryColorScheme,
|
||||
};
|
||||
// A map of color schemes supported by the app
|
||||
final Map<String, ColorScheme> _supportedColorScheme = {'primary': ColorSchemes.primaryColorScheme};
|
||||
|
||||
/// Returns the primary colors for the current theme.
|
||||
PrimaryColors _getThemeColors() {
|
||||
//throw exception to notify given theme is not found or not generated by the generator
|
||||
if (!_supportedCustomColor.containsKey(_appTheme)) {
|
||||
throw Exception(
|
||||
"$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
|
||||
);
|
||||
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
|
||||
}
|
||||
//return theme from map
|
||||
|
||||
@@ -34,15 +28,14 @@ class ThemeHelper {
|
||||
ThemeData _getThemeData() {
|
||||
//throw exception to notify given theme is not found or not generated by the generator
|
||||
if (!_supportedColorScheme.containsKey(_appTheme)) {
|
||||
throw Exception(
|
||||
"$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
|
||||
);
|
||||
throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
|
||||
}
|
||||
//return theme from map
|
||||
|
||||
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
|
||||
return ThemeData(
|
||||
textTheme: GoogleFonts.mulishTextTheme(),
|
||||
fontFamily: 'Mulish-Local',
|
||||
textTheme: ThemeData.light().textTheme.apply(fontFamily: 'Mulish-Local'),
|
||||
visualDensity: VisualDensity.standard,
|
||||
colorScheme: colorScheme,
|
||||
scaffoldBackgroundColor: colorScheme.onPrimary,
|
||||
@@ -60,17 +53,9 @@ class ThemeHelper {
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
side: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 1,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
vertical: -4,
|
||||
horizontal: -4,
|
||||
),
|
||||
side: BorderSide(color: colorScheme.primary, width: 1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
@@ -101,7 +86,7 @@ class PrimaryColors {
|
||||
// Blue
|
||||
Color get blue400 => const Color(0XFF43A6F2);
|
||||
|
||||
Color get blueA400 => const Color(0XFF2F80ED);
|
||||
Color get blueA400 => const Color(0XFF2F80ED);
|
||||
// BlueGray
|
||||
Color get blueGray400 => const Color(0XFF7B8794);
|
||||
|
||||
@@ -141,12 +126,17 @@ class PrimaryColors {
|
||||
|
||||
Color get gray60003 => const Color(0XFF707070);
|
||||
|
||||
Color get gray60005 => const Color(0xFF6C7C93);
|
||||
|
||||
// Green
|
||||
Color get greenA700 => const Color(0XFF019A3E);
|
||||
|
||||
Color get greenA70001 => const Color(0XFF00BE13);
|
||||
|
||||
Color get green => const Color(0XFF0BB450);
|
||||
Color get darkGreen => const Color(0XFF16A249);
|
||||
|
||||
Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
|
||||
|
||||
// LightGreen
|
||||
Color get lightGreen300 => const Color(0XFFB2DF8A);
|
||||
@@ -180,6 +170,8 @@ class PrimaryColors {
|
||||
|
||||
Color get lightGray0 => const Color(0xFFF6F6F6);
|
||||
|
||||
Color get darkGrey => const Color(0xFF5B656F);
|
||||
|
||||
Color get lightGray00 => const Color(0xFFE9E9EA);
|
||||
|
||||
Color get dottedBorder => const Color(0xFFE6E6E6);
|
||||
@@ -206,6 +198,8 @@ class PrimaryColors {
|
||||
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
|
||||
|
||||
Color get grey7B => const Color(0xFF7B7B7B);
|
||||
|
||||
Color get greyD9 => const Color(0xFFD9D9D9);
|
||||
}
|
||||
|
||||
PrimaryColors get appTheme => ThemeHelper().themeColor();
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
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 {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
final sharedPreferences = await _getPrefs();
|
||||
if (sharedPreferences.getString('token') == null) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -15,25 +30,25 @@ abstract class TokenStorage {
|
||||
}
|
||||
|
||||
static Future<String?> getTokenFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await _getPrefs();
|
||||
return sharedPreferences.getString('token');
|
||||
}
|
||||
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await _getPrefs();
|
||||
await sharedPreferences.setString('token', token);
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await _getPrefs();
|
||||
await sharedPreferences.remove('token');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SocketIdStorage {
|
||||
static Future<bool> haveSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
if (sharedPreferences.getString('socketId') == null) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
|
||||
}
|
||||
|
||||
static Future<String?> getSocketIdFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
return sharedPreferences.getString('socketId');
|
||||
}
|
||||
|
||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
await sharedPreferences.setString('socketId', socketId);
|
||||
}
|
||||
|
||||
static Future<void> removeSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final sharedPreferences = await TokenStorage._getPrefs();
|
||||
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 {
|
||||
String capitalize() {
|
||||
if (isEmpty) return '';
|
||||
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
||||
}
|
||||
|
||||
String parseGender() {
|
||||
if (this == 'male') {
|
||||
return 'Мужской';
|
||||
return 'Мужской';
|
||||
} else if (this == 'female') {
|
||||
return 'Женский';
|
||||
}
|
||||
|
||||
@@ -16,6 +16,21 @@ class ValidationUtils {
|
||||
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
|
||||
static bool isValidUrl(String url) {
|
||||
if (url.isEmpty) return false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/core/views/service/views_service.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class ViewsRepository {
|
||||
final ViewsService viewsService;
|
||||
@@ -9,7 +10,8 @@ class ViewsRepository {
|
||||
Future<void> viewContent({required ViewModel viewModel}) async {
|
||||
try {
|
||||
await viewsService.viewContent(viewModel: viewModel);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
@@ -8,7 +9,9 @@ import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.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 {
|
||||
final WsService wsService;
|
||||
@@ -22,6 +25,10 @@ class WsManager {
|
||||
PrivateChannel? _privateMessagesChannel;
|
||||
PrivateChannel? _privateDirectChatChannel;
|
||||
|
||||
StreamSubscription? _lifecycleSubscription;
|
||||
StreamSubscription? _eventSubscription;
|
||||
StreamSubscription? _connectionEstablishedSubscription;
|
||||
|
||||
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
|
||||
|
||||
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
|
||||
@@ -33,7 +40,7 @@ class WsManager {
|
||||
Future<void> initialize() async {
|
||||
if (client == null) {
|
||||
try {
|
||||
await TokenStorage.haveTokenInStorage();
|
||||
if (!await TokenStorage.haveTokenInStorage()) return;
|
||||
final wsUrl = await wsService.getWebsocket();
|
||||
|
||||
final options = PusherChannelsOptions.fromHost(
|
||||
@@ -71,7 +78,7 @@ class WsManager {
|
||||
}
|
||||
}
|
||||
|
||||
client?.eventStream.listen((event) async {
|
||||
_connectionEstablishedSubscription = client?.eventStream.listen((event) async {
|
||||
if (event.name == 'pusher:connection_established') {
|
||||
final data = event.data;
|
||||
final socketId = extractSocketId(data);
|
||||
@@ -87,26 +94,46 @@ class WsManager {
|
||||
await _initPrivateNotificationsChannel();
|
||||
await _initPrivateMessagesChannel();
|
||||
|
||||
client?.lifecycleStream.listen((data) {
|
||||
_lifecycleSubscription = client?.lifecycleStream.listen((data) {
|
||||
if (data == PusherChannelsClientLifeCycleState.establishedConnection) {
|
||||
_unsubscribe();
|
||||
_subscribe();
|
||||
}
|
||||
});
|
||||
client?.eventStream.listen((event) {
|
||||
EasyDebounce.debounce(
|
||||
'searchChatRoom',
|
||||
const Duration(milliseconds: 300),
|
||||
() => sl<ChatsCubit>().getChatRooms(),
|
||||
);
|
||||
_eventSubscription = client?.eventStream.listen((event) {
|
||||
log('WsManager received event: ${event.name}');
|
||||
// Только если это не системные сообщения Pusher, обновляем список чатов
|
||||
if (!event.name.startsWith('pusher:')) {
|
||||
EasyDebounce.debounce(
|
||||
'searchChatRoom',
|
||||
const Duration(milliseconds: 300),
|
||||
() => sl<ChatsCubit>().getChatRooms(),
|
||||
);
|
||||
}
|
||||
});
|
||||
_subscribe();
|
||||
await _connectCubitsToStreams();
|
||||
} catch (e, st) {
|
||||
await Sentry.captureException(e, 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 Sentry.captureException(e, stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initPrivateNotificationsChannel() async {
|
||||
final token = await TokenStorage.getTokenFromStorage();
|
||||
_privateNotificationChannel = client?.privateChannel(
|
||||
@@ -166,6 +193,12 @@ class WsManager {
|
||||
|
||||
Future<void> clear() async {
|
||||
await client?.disconnect();
|
||||
await _lifecycleSubscription?.cancel();
|
||||
await _eventSubscription?.cancel();
|
||||
await _connectionEstablishedSubscription?.cancel();
|
||||
_lifecycleSubscription = null;
|
||||
_eventSubscription = null;
|
||||
_connectionEstablishedSubscription = null;
|
||||
client = null;
|
||||
userId = null;
|
||||
_privateDirectChatChannel?.unsubscribe();
|
||||
|
||||
@@ -3,16 +3,28 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class CustomDropDownButton<T> extends StatelessWidget {
|
||||
const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
|
||||
const CustomDropDownButton({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
this.value,
|
||||
this.hint,
|
||||
this.selectedItemBuilder,
|
||||
this.hasError = false,
|
||||
this.isExpanded = false,
|
||||
});
|
||||
final T? value;
|
||||
final List<DropdownMenuItem<T>> items;
|
||||
final void Function(T?)? onChanged;
|
||||
final Widget? hint;
|
||||
final bool hasError;
|
||||
final List<Widget> Function(BuildContext)? selectedItemBuilder;
|
||||
final bool isExpanded;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField(
|
||||
hint: hint,
|
||||
isExpanded: isExpanded,
|
||||
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
dropdownColor: appTheme.lightGray0,
|
||||
@@ -35,6 +47,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
||||
initialValue: value,
|
||||
items: items,
|
||||
onChanged: onChanged,
|
||||
selectedItemBuilder: selectedItemBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class CustomOutlinedButton extends BaseButton {
|
||||
margin: margin,
|
||||
decoration: decoration,
|
||||
child: OutlinedButton(
|
||||
style: buttonStyle!,
|
||||
style: buttonStyle ?? OutlinedButton.styleFrom(),
|
||||
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -59,7 +59,7 @@ class CustomOutlinedButton extends BaseButton {
|
||||
margin: margin,
|
||||
decoration: decoration,
|
||||
child: OutlinedButton(
|
||||
style: buttonStyle!,
|
||||
style: buttonStyle ?? OutlinedButton.styleFrom(),
|
||||
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -27,7 +27,7 @@ class FeedIconButton extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (svgIcon != null) svgIcon!,
|
||||
?svgIcon,
|
||||
if (icon != null)
|
||||
Icon(
|
||||
icon,
|
||||
|
||||
@@ -12,15 +12,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class InfoRow extends StatelessWidget {
|
||||
const InfoRow({
|
||||
required this.type,
|
||||
super.key,
|
||||
this.diet,
|
||||
this.itemsBackgroundColor,
|
||||
this.disableDescriptionOfTypes = false,
|
||||
this.plan,
|
||||
this.recipe,
|
||||
});
|
||||
const InfoRow({required this.type, super.key, this.diet, this.itemsBackgroundColor, this.disableDescriptionOfTypes = false, this.plan, this.recipe});
|
||||
final ProductType type;
|
||||
final Color? itemsBackgroundColor;
|
||||
final Diet? diet;
|
||||
@@ -50,44 +42,13 @@ class InfoRow extends StatelessWidget {
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/time2.svg',
|
||||
width: 13.3,
|
||||
height: 13.3,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/time2.svg', width: 13.3, height: 13.3),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
switch (type) {
|
||||
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
||||
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
||||
ProductType.recipe => timeTitle ?? '',
|
||||
},
|
||||
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),
|
||||
),
|
||||
Text(switch (type) {
|
||||
ProductType.diet => '${diet?.duration} ${getDayForm(diet?.duration ?? 7, s)}',
|
||||
ProductType.plan => '${plan?.duration} ${getDayForm(plan?.duration ?? 7, s)}',
|
||||
ProductType.recipe => timeTitle ?? '',
|
||||
}, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -95,11 +56,17 @@ class InfoRow extends StatelessWidget {
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/icons/steps.svg',
|
||||
width: 13.3,
|
||||
height: 13.3,
|
||||
),
|
||||
SvgPicture.asset('assets/icons/calories.svg', width: 13.3, height: 13.3),
|
||||
const SizedBox(width: 4),
|
||||
Text('${(recipe?.calories ?? 0).toStringAsFixed(0)} ккал', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
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),
|
||||
Text(
|
||||
'${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)
|
||||
InfoBouble(
|
||||
backgroundColor: itemsBackgroundColor,
|
||||
children: [
|
||||
TypeBadges(
|
||||
type: diet?.type,
|
||||
planType: plan?.type,
|
||||
disableDescriptionOfTypes: disableDescriptionOfTypes,
|
||||
),
|
||||
],
|
||||
children: [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,
|
||||
);
|
||||
}
|
||||
}
|
||||
124
lib/core/widgets/marketing_bottom_sheet.dart
Normal file
124
lib/core/widgets/marketing_bottom_sheet.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
void showMarketingBottomSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
|
||||
child: const MarketingBottomSheet(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MarketingBottomSheet extends StatelessWidget {
|
||||
const MarketingBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsetsGeometry.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(image: AssetImage('assets/images/logo_bg.png'), fit: BoxFit.cover),
|
||||
),
|
||||
child: Center(child: SvgPicture.asset('assets/icons/logo_2.svg', height: 90)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(s.congratulations, style: TextStyles.textFS24FW700.copyWith(color: appTheme.blueA400)),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: appTheme.lightGray0, borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: appTheme.blueA400),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text('14', style: TextStyles.textFS24FW700.copyWith(color: appTheme.whiteA700)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: s.youWithUs,
|
||||
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.gray60005),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: s.fourteenDays,
|
||||
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
s.weAreLookingFor,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
s.letsInvite,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.invite,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
leftIcon: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Transform.rotate(
|
||||
angle: 3.14,
|
||||
child: SvgPicture.asset('assets/icons/share.svg', width: 14, height: 14, colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn)),
|
||||
),
|
||||
),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
SharePlus.instance.share(ShareParams(uri: Uri.parse('https://promo.eatvio.ru/')));
|
||||
},
|
||||
),
|
||||
),
|
||||
Text(
|
||||
s.eatvioIsAHelthyHabbit,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,10 @@ import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionWidget extends StatelessWidget {
|
||||
const OptionWidget({super.key, required this.option, this.disableDuration = false});
|
||||
const OptionWidget({super.key, required this.option, this.disableDuration = false, required this.duration});
|
||||
final PacketOptions? option;
|
||||
final bool disableDuration;
|
||||
final int duration;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (option == null) {
|
||||
@@ -34,8 +35,12 @@ class OptionWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (!disableDuration) ...[
|
||||
const _OptionDurationWidget(duration: 7),
|
||||
const SizedBox(height: 16),
|
||||
_OptionDurationWidget(duration: duration),
|
||||
SizedBox(height: option?.hasPersonalDiet ?? false ? 8 : 12),
|
||||
],
|
||||
if ((option?.hasPersonalDiet ?? false) && !disableDuration) ...[
|
||||
const _OptionHasPersonalDietWidget(),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
const CustomDivider(),
|
||||
const SizedBox(height: 16),
|
||||
@@ -52,7 +57,6 @@ class OptionWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _OptionDurationWidget extends StatelessWidget {
|
||||
const _OptionDurationWidget({required this.duration});
|
||||
final int duration;
|
||||
@@ -92,3 +96,43 @@ class _OptionDurationWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionHasPersonalDietWidget extends StatelessWidget {
|
||||
const _OptionHasPersonalDietWidget();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: appTheme.transparentGreen),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Container(
|
||||
width: 4,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.green,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(100),
|
||||
bottomRight: Radius.circular(100),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(
|
||||
s.personalDietCreating,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/theme_helper.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
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,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
Navigator.of(context).pop();
|
||||
if (needToOpenSettings ?? false) {
|
||||
openAppSettings();
|
||||
}
|
||||
|
||||
@@ -8,32 +8,33 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
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 bool isPlan;
|
||||
final bool? isDrafted;
|
||||
final bool? isUpdated;
|
||||
@override
|
||||
Widget build(BuildContext 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(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const BottomSheetHandlerBar(),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(
|
||||
isPlan ? 'опубликован' : 'опубликованна',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(title, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
Text(text, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: CustomOutlinedButton(
|
||||
@@ -43,15 +44,10 @@ class SuccessPublishedDietOrPlanBottomSheet extends StatelessWidget {
|
||||
text: s.ok,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textSSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(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,23 @@
|
||||
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/utils/toasts.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/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
|
||||
class AuthCubit extends Cubit<AuthState> {
|
||||
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 {
|
||||
emit(const AuthState.loading());
|
||||
@@ -25,4 +33,26 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initializeUser() async {
|
||||
try {
|
||||
await registerAnonymousPushToken();
|
||||
FlutterNativeSplash.remove();
|
||||
} catch (e) {
|
||||
log('Error in initializeUser: $e');
|
||||
FlutterNativeSplash.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerAnonymousPushToken() async {
|
||||
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
||||
if (!hasAnonymousTokenBeenSent) {
|
||||
await firebaseApi.registerAnonymousPushToken();
|
||||
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPushToken() async {
|
||||
await firebaseApi.registerPushToken();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'dart:developer';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
|
||||
import 'package:eatvio_app/core/prefs.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/repositories/auth_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class OAuthCubit extends Cubit<OAuthState> {
|
||||
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
|
||||
final AuthRepository authRepository;
|
||||
|
||||
OAuthCubit({required this.authRepository}) : super(const OAuthState());
|
||||
OAuthCubit({required this.authRepository, required Prefs prefs}) : super(const OAuthState()) {
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
|
||||
try {
|
||||
@@ -24,6 +29,39 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
log('OAuthCubit: Got token model: ${tokenModel.token}');
|
||||
TokenStorage.saveTokenInStorage(tokenModel.token);
|
||||
emit(OAuthState.loaded(token: tokenModel.token));
|
||||
} on DioException catch (e) {
|
||||
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) {
|
||||
log('OAuthCubit: Error occurred: $e');
|
||||
emit(OAuthState.error(errorMessage: e.toString()));
|
||||
@@ -63,6 +101,7 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
clientId: clientId,
|
||||
);
|
||||
TokenStorage.saveTokenInStorage(token);
|
||||
cacheRegistrationDate(DateTime.now());
|
||||
emit(OAuthState.loaded(token: token));
|
||||
} on DioException catch (e) {
|
||||
emit(OAuthState.error(errorMessage: e.toString()));
|
||||
@@ -72,16 +111,22 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
final dateOfBirth = errors?['date_of_birth'] as List<dynamic>?;
|
||||
final errorsList = <RegistrationValidationErrors>[];
|
||||
if (emailErrors != null && emailErrors.isNotEmpty) {
|
||||
if (emailErrors.contains('Некорректный формат email')) {
|
||||
errorsList.add(RegistrationValidationErrors.incorrectEmailFormat);
|
||||
}
|
||||
if (emailErrors.contains('Такой Эл. почта уже существует.')) {
|
||||
errorsList.add(RegistrationValidationErrors.emailExist);
|
||||
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) {
|
||||
if (phoneErrors.contains('Такой phone уже существует.')) {
|
||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||
for (final error in phoneErrors) {
|
||||
final errorStr = error.toString();
|
||||
if (errorStr.contains('phone уже существует') || errorStr.contains('телефон уже существует')) {
|
||||
errorsList.add(RegistrationValidationErrors.phoneExist);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dateOfBirth != null && dateOfBirth.isNotEmpty) {
|
||||
@@ -96,6 +141,70 @@ class OAuthCubit extends Cubit<OAuthState> {
|
||||
|
||||
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) {
|
||||
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) {
|
||||
log('OAuthCubit: Error confirming OAuth verification: $e');
|
||||
emit(OAuthState.error(errorMessage: e.toString()));
|
||||
rethrow; // Пробрасываем ошибку дальше, чтобы UI мог её обработать
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.error({required String errorMessage}) = OAuthStateError;
|
||||
const factory OAuthState.error({
|
||||
required String errorMessage,
|
||||
List<RegistrationValidationErrors>? registrationValidationErrors,
|
||||
}) = OAuthStateError;
|
||||
}
|
||||
|
||||
enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat }
|
||||
|
||||
@@ -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/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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -81,9 +79,6 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
@@ -214,9 +209,6 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
await sl<WsManager>().initialize();
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешный вход');
|
||||
} catch (e) {
|
||||
@@ -232,9 +224,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
final bool useMobileLayout = shortestSide < 600;
|
||||
if (!useMobileLayout) isFocused = true;
|
||||
final s = S.of(context);
|
||||
FlutterNativeSplash.remove();
|
||||
return BlocProvider<AuthCubit>(
|
||||
create: (BuildContext context) => authCubit,
|
||||
create: (BuildContext context) => authCubit..initializeUser(),
|
||||
child: BlocBuilder<AuthCubit, AuthState>(
|
||||
builder: (BuildContext context, AuthState state) {
|
||||
return Scaffold(
|
||||
@@ -337,7 +328,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
children: [
|
||||
const Text('Нет аккаунта? '),
|
||||
GestureDetector(
|
||||
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
|
||||
onTap: () => GoRouter.of(context).pushNamed('registrationScreenV2'),
|
||||
child: Text(
|
||||
'Зарегистрироваться',
|
||||
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
@@ -411,7 +402,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
label: state is AuthStateLoading || _isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||
),
|
||||
@@ -426,8 +417,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
||||
onPressed: state is AuthStateLoading
|
||||
backgroundColor: state is AuthStateLoading || _isLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
||||
onPressed: state is AuthStateLoading || _isLoading
|
||||
? null
|
||||
: () async {
|
||||
await showDialog(
|
||||
@@ -487,7 +478,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
);
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
label: state is AuthStateLoading || _isLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
|
||||
),
|
||||
|
||||
@@ -1,166 +1,124 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
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_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ForgetPasswordScreen extends StatefulWidget {
|
||||
final bool isAuth;
|
||||
const ForgetPasswordScreen({super.key, required this.isAuth});
|
||||
|
||||
@override
|
||||
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
|
||||
}
|
||||
|
||||
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
final emailController = TextEditingController();
|
||||
final codeController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final passwordConfirmController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ResetPasswordCubit>(
|
||||
create: (BuildContext context) => sl<ResetPasswordCubit>(),
|
||||
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
||||
builder: (BuildContext context, ResetPasswordState state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
||||
Text(
|
||||
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'E-mail',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: emailController,
|
||||
hintText: 'Введите e-mail',
|
||||
),
|
||||
],
|
||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
||||
const Text(
|
||||
'Код',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: codeController,
|
||||
hintText: 'Введите код',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Пароль',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: passwordController,
|
||||
hintText: 'Введите новый пароль',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Подтвердите пароль',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: passwordConfirmController,
|
||||
hintText: 'Введите новый пароль',
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
||||
context.read<ResetPasswordCubit>().tryResetPassword(
|
||||
emailController.text,
|
||||
codeController.text,
|
||||
passwordController.text,
|
||||
passwordConfirmController.text,
|
||||
);
|
||||
} else {
|
||||
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
||||
? 'Сменить пароль'
|
||||
: 'Сбросить пароль',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
listener: (BuildContext context, ResetPasswordState state) {
|
||||
if (state is ResetPasswordStateCodeSendSuccess) {
|
||||
showSuccessToast('Код на почту успешно отправлен');
|
||||
}
|
||||
if (state is ResetPasswordStateCodeSendError) {
|
||||
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
|
||||
}
|
||||
if (state is ResetPasswordStateResetPasswordError) {
|
||||
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
|
||||
}
|
||||
if (state is ResetPasswordStateResetPassowrdSuccess) {
|
||||
GoRouter.of(context).pop();
|
||||
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
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_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ForgetPasswordScreen extends StatefulWidget {
|
||||
final bool isAuth;
|
||||
const ForgetPasswordScreen({super.key, required this.isAuth});
|
||||
|
||||
@override
|
||||
State<ForgetPasswordScreen> createState() => _ForgetPasswordScreenState();
|
||||
}
|
||||
|
||||
class _ForgetPasswordScreenState extends State<ForgetPasswordScreen> {
|
||||
final emailController = TextEditingController();
|
||||
final codeController = TextEditingController();
|
||||
final passwordController = TextEditingController();
|
||||
final passwordConfirmController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ResetPasswordCubit>(
|
||||
create: (BuildContext context) => sl<ResetPasswordCubit>(),
|
||||
child: BlocConsumer<ResetPasswordCubit, ResetPasswordState>(
|
||||
builder: (BuildContext context, ResetPasswordState state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: widget.isAuth ? const Text('Смена пароля') : const Text('Восстановление пароля'), centerTitle: false),
|
||||
body: state is ResetPasswordStateLoadingResetPassword || state is ResetPasswordStateLoadingSendEmailCode
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (state is ResetPasswordStateCodeSendError || state is ResetPasswordStateEmpty) ...[
|
||||
Text(
|
||||
'${widget.isAuth ? '' : 'Забыли пароль? '}Укажите свой email,\nи мы пришлём код для сброса.',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text('E-mail', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: emailController, hintText: 'Введите e-mail'),
|
||||
],
|
||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) ...[
|
||||
const Text('Код', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: codeController, hintText: 'Введите код'),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: passwordController,
|
||||
hintText: 'Введите новый пароль',
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Подтвердите пароль', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: passwordConfirmController,
|
||||
hintText: 'Введите новый пароль',
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError) {
|
||||
context.read<ResetPasswordCubit>().tryResetPassword(
|
||||
emailController.text,
|
||||
codeController.text,
|
||||
passwordController.text,
|
||||
passwordConfirmController.text,
|
||||
);
|
||||
} else {
|
||||
context.read<ResetPasswordCubit>().trySendEmailCode(emailController.text);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: Center(
|
||||
child: Text(
|
||||
state is ResetPasswordStateCodeSendSuccess || state is ResetPasswordStateResetPasswordError
|
||||
? 'Сменить пароль'
|
||||
: 'Сбросить пароль',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
listener: (BuildContext context, ResetPasswordState state) {
|
||||
if (state is ResetPasswordStateCodeSendSuccess) {
|
||||
showSuccessToast('Код на почту успешно отправлен');
|
||||
}
|
||||
if (state is ResetPasswordStateCodeSendError) {
|
||||
showErrorToast('Ошибка отправки кода. Проверьте правильность ввёдной почты');
|
||||
}
|
||||
if (state is ResetPasswordStateResetPasswordError) {
|
||||
showErrorToast('Ошибка смены пароля. Проверьте правильность ввёднного кода и совпадение пароля');
|
||||
}
|
||||
if (state is ResetPasswordStateResetPassowrdSuccess) {
|
||||
GoRouter.of(context).pop();
|
||||
showSuccessToast('Пароль успешно сменён. ${widget.isAuth ? '' : 'Войдите в аккаунт'}');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:developer';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/const.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/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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.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/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -49,6 +50,8 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
bool weightError = false;
|
||||
bool heightError = false;
|
||||
bool incorrectEmailFormatError = false;
|
||||
bool showVerificationButton = false;
|
||||
String? verificationType; // 'email' or 'phone'
|
||||
|
||||
// Состояние для выбора пола
|
||||
bool isMaleSelected = true;
|
||||
@@ -70,9 +73,6 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
showSuccessToast('Успешная регистрация');
|
||||
} catch (e) {
|
||||
@@ -221,7 +221,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
hasError: emailExistError || incorrectEmailFormatError,
|
||||
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')],
|
||||
const SizedBox(height: 12),
|
||||
const Text('Номер телефона'),
|
||||
@@ -235,7 +256,28 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
hasError: phoneExistError,
|
||||
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),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -289,12 +331,17 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
width: MediaQuery.of(context).size.width / 2 - 22,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Вес'),
|
||||
const Text('Рост'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError),
|
||||
if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')],
|
||||
CustomTextFormField(
|
||||
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,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Рост'),
|
||||
const Text('Вес'),
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError),
|
||||
if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')],
|
||||
CustomTextFormField(
|
||||
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;
|
||||
|
||||
// Очищаем номер телефона от маски для отправки
|
||||
final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), '');
|
||||
final cleanPhone = ValidationUtils.normalizePhone(phoneController.text);
|
||||
|
||||
if (clientID == null) return;
|
||||
// Вызываем OAuth регистрацию
|
||||
@@ -501,6 +555,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
clientId: clientID,
|
||||
);
|
||||
if (errors != null) {
|
||||
bool hasEmailExist = false;
|
||||
bool hasPhoneExist = false;
|
||||
|
||||
// Сначала определяем, какие ошибки присутствуют
|
||||
for (final error in errors) {
|
||||
switch (error) {
|
||||
case RegistrationValidationErrors.incorrectEmailFormat:
|
||||
@@ -509,14 +567,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
});
|
||||
break;
|
||||
case RegistrationValidationErrors.emailExist:
|
||||
setState(() {
|
||||
emailExistError = true;
|
||||
});
|
||||
hasEmailExist = true;
|
||||
break;
|
||||
case RegistrationValidationErrors.phoneExist:
|
||||
setState(() {
|
||||
phoneExistError = true;
|
||||
});
|
||||
hasPhoneExist = true;
|
||||
break;
|
||||
case RegistrationValidationErrors.incorrectDateOfBirthFormat:
|
||||
setState(() {
|
||||
@@ -525,8 +579,236 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
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;
|
||||
}
|
||||
// Регистрация успешна только если нет ошибок
|
||||
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/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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||
@@ -76,9 +75,6 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
||||
try {
|
||||
await sl<ProfileAvatarCubit>().getMyProfile();
|
||||
await sl<ChatsCubit>().getChatRoomsWithLoading();
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import 'dart:developer';
|
||||
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_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/services/auth_service.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class AuthRepository {
|
||||
final AuthService authService;
|
||||
@@ -14,12 +19,12 @@ class AuthRepository {
|
||||
return await authService.getTokenByCredentials(
|
||||
credentials: CredentialsModel(email: email, password: password),
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<TokenModel> signInViaSocialMedia({
|
||||
required String provider,
|
||||
required String accessToken,
|
||||
@@ -30,7 +35,8 @@ class AuthRepository {
|
||||
return await authService.signInViaSocialMedia(
|
||||
body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId),
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +75,73 @@ class AuthRepository {
|
||||
),
|
||||
))['token']!
|
||||
as String;
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
|
||||
try {
|
||||
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;
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
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);
|
||||
} catch (e, s) {
|
||||
log('AuthRepository: Error in confirmOAuthVerification: $e');
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/features/auth/services/reset_password_service.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class ResetPasswordRepository {
|
||||
final ResetPasswordService resetPasswordService;
|
||||
@@ -20,7 +21,8 @@ class ResetPasswordRepository {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +48,8 @@ class ResetPasswordRepository {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:vk_id/vk_id.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
@@ -40,7 +42,8 @@ class SocialAuthFacade {
|
||||
return null;
|
||||
}
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -156,8 +159,9 @@ class SocialAuthFacade {
|
||||
birthDate: yandexProfileData.birthday,
|
||||
clientId: clientId,
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
log(e.toString());
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -204,7 +208,8 @@ class SocialAuthFacade {
|
||||
if (code == null || deviceId == null) return null;
|
||||
|
||||
return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -255,8 +260,9 @@ class SocialAuthFacade {
|
||||
if (!context.mounted) return null;
|
||||
return showDialog<Map<String, String>>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) {
|
||||
barrierDismissible: true,
|
||||
builder: (dialogContext) {
|
||||
final s = S.of(dialogContext);
|
||||
const borderRadius = BorderRadius.all(Radius.circular(16));
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
@@ -264,7 +270,21 @@ class SocialAuthFacade {
|
||||
elevation: 0,
|
||||
child: ClipRRect(
|
||||
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/oauth_model.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:retrofit/retrofit.dart';
|
||||
|
||||
@@ -19,4 +22,10 @@ abstract class AuthService {
|
||||
|
||||
@POST('/api/oauth/login')
|
||||
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);
|
||||
}
|
||||
112
lib/features/barcode/repositories/barcode_repository.dart
Normal file
112
lib/features/barcode/repositories/barcode_repository.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
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';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class BarcodeRepository {
|
||||
final BarcodeService barcodeService;
|
||||
final Dio dio;
|
||||
|
||||
BarcodeRepository({
|
||||
required this.barcodeService,
|
||||
required this.dio,
|
||||
});
|
||||
|
||||
/// Получить информацию о продукте по штрихкоду
|
||||
/// Возвращает либо данные продукта, либо статус задачи обработки
|
||||
Future<BarcodeResult> getProductByBarcode(String barcode) async {
|
||||
try {
|
||||
final response = await dio.get('/api/gsone/barcode/$barcode');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Данные найдены в БД
|
||||
final product = BarcodeProductModel.fromJson(response.data as Map<String, dynamic>);
|
||||
return BarcodeResult.success(product);
|
||||
} else if (response.statusCode == 202) {
|
||||
// Запущена задача обработки
|
||||
final taskStatus = BarcodeTaskStatusModel.fromJson(response.data as Map<String, dynamic>);
|
||||
return BarcodeResult.processing(taskStatus.taskId ?? '');
|
||||
}
|
||||
|
||||
throw Exception('Unexpected status code: ${response.statusCode}');
|
||||
} on DioException catch (e, s) {
|
||||
if (e.response?.statusCode == 202) {
|
||||
// Если получен статус 202, значит запущена задача обработки
|
||||
final taskStatus = BarcodeTaskStatusModel.fromJson(e.response!.data as Map<String, dynamic>);
|
||||
return BarcodeResult.processing(taskStatus.taskId ?? '');
|
||||
}
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить статус задачи обработки
|
||||
Future<BarcodeTaskStatus> getTaskStatus(String taskId) async {
|
||||
try {
|
||||
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();
|
||||
}
|
||||
} catch (e, s) {
|
||||
await Sentry.captureException(e, stackTrace: s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Результат запроса информации о продукте по штрихкоду
|
||||
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,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user