Compare commits

...

387 Commits

Author SHA1 Message Date
misha
9de0efa28e fix: test fixed 2026-08-03 15:30:54 +03:00
misha
2d6b042d45 fix: removed unused import 2026-07-28 12:00:03 +03:00
misha
b3d0a26667 Merge remote-tracking branch 'upstream/main' into personalized_feed 2026-07-23 15:40:57 +03:00
8835d65944 Merge pull request 'version changed' (#1007) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#1007
2026-07-23 12:37:37 +00:00
misha
cc9aa6bffb version changed 2026-07-23 15:36:02 +03:00
a15e8b78a5 Merge pull request 'Info plist + version fix' (#1006) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#1006
2026-07-23 12:32:14 +00:00
misha
0667ab81a8 Merge remote-tracking branch 'upstream/main' 2026-07-23 15:31:23 +03:00
misha
1dc60eec64 version changed 2026-07-23 15:29:47 +03:00
misha
69f0f1e919 fix: removed unused info plist key 2026-07-23 15:27:49 +03:00
misha
a6fe6afb80 feat(personalized_feed): fixed photos rendering and fixed profile posts rendering 2026-07-23 15:17:01 +03:00
misha
c8c8ade083 Merge remote-tracking branch 'upstream/main' into personalized_feed 2026-07-23 15:15:59 +03:00
misha
2cccc9e253 feat(personalized_feed): added video and multiple images handling to detailed blog post screen 2026-07-22 14:41:26 +03:00
dcf7b25989 Merge pull request 'пофиксили отображение коунтера карусели при переключении картинок, при посте с одной картинкой и одним видео. добавили таймер на видео.' (#1002) from Hasan/eatvio_app:personalized_feed into personalized_feed
Reviewed-on: eatvio/eatvio_app#1002
2026-07-22 07:26:28 +00:00
163f56ff61 Merge pull request 'feat(vidoe_calls)' (#983) from misha/eatvio_app:issue-975 into main
Reviewed-on: eatvio/eatvio_app#983
2026-07-22 07:25:32 +00:00
d6b57ef34d пофиксили отображение коунтера карусели при переключении картинок, при посте с одной картинкой и одним видео. добавили таймер на видео. 2026-07-21 16:50:06 +03:00
45f20cc903 Merge pull request 'personalized_feed' (#1) from eatvio/eatvio_app:personalized_feed into personalized_feed
Reviewed-on: Hasan/eatvio_app#1
2026-07-21 12:43:51 +00:00
746b9b6d28 Merge pull request 'feat(personalized_feed): added vk player state handling, separeted player logic' (#1001) from misha/eatvio_app:personalized_feed into personalized_feed
Reviewed-on: eatvio/eatvio_app#1001
2026-07-21 12:41:33 +00:00
misha
2dbd4a455f feat(personalized_feed): added vk player state handling, separeted player logic 2026-07-21 15:15:28 +03:00
c7514e2cbf Merge pull request 'fix fixtures' (#1000) from Hasan/eatvio_app:personalized_feed into personalized_feed
Reviewed-on: eatvio/eatvio_app#1000
2026-07-21 11:26:44 +00:00
f1abea70c5 fix fixtures 2026-07-21 14:22:45 +03:00
e4d397b478 Merge pull request 'personalized_feed with video and multiple images' (#999) from apoloz/eatvio_app:personalized_feed into personalized_feed
Reviewed-on: eatvio/eatvio_app#999
2026-07-21 08:08:19 +00:00
9c5c6df751 refactor(video,webview): webview lib replaced on feed 2026-07-20 18:15:25 +03:00
misha
48a984f5e9 feat(personalized_feed): added fixtures for new feed, added new feed parsing and layout, added webview player for post videos 2026-07-20 16:47:19 +03:00
misha
5dc1f48fd1 feat(ci/cd): added api key for tf 2026-07-15 17:31:32 +03:00
misha
5ec877ef72 version changed 2026-07-15 15:19:56 +03:00
misha
dbaf639ac6 fix: analyzer fix 2026-07-15 15:18:53 +03:00
misha
2a07ed596b Merge remote-tracking branch 'upstream/main' into issue-975 2026-07-15 15:17:44 +03:00
bd2de07b8f Merge pull request 'sbp-alfa-bank-payment' (#998) from misha/eatvio_app:sbp_payments into main
Reviewed-on: eatvio/eatvio_app#998
2026-07-15 12:06:00 +00:00
misha
eee912f05b fix: pub cache cleaning 2026-07-14 16:57:32 +03:00
misha
e316bf1e0e version changed 2026-07-14 16:12:43 +03:00
misha
de19af14bf fix: fixed analyzer and test 2026-07-14 16:11:56 +03:00
Anatoliy
b064c7016e feat(payment, chat): promo code mocked BottomSheet added 2026-07-14 15:50:44 +03:00
misha
e94681266a feat(sbp_payments): added new layoute for sbp banks and rederict to chosen bank, and added tests for layoute 2026-07-14 13:43:46 +03:00
misha
0ca2d1a1cc feat(sbp_payments): added banks deep link handling to chat screen 2026-07-14 13:09:18 +03:00
misha
be924265a1 feat(sbp_payments): added cubit layer for sbp payments with tests 2026-07-09 18:35:09 +03:00
a0edfe942b feat(payments): phase 3 test added 2026-07-09 16:21:53 +03:00
misha
51397003b5 feat(sbp_payments): added tests for new chosen_plan_state fixtures and new tests for chat repo and service 2026-07-08 15:07:36 +03:00
misha
e10610d93c feat(sbp_payments): added new chosen_plan fixtures with chosen_plan states and fixed chat tests 2026-07-08 14:28:08 +03:00
Anatoliy Poloz
d21cc46558 feat(payments): phase #1 tests and part of implementation 2026-07-08 13:31:55 +03:00
Anatoliy Poloz
2ab26bbda5 feat(chat): reorganize test fixtures 2026-07-08 12:31:06 +03:00
misha
41e556d172 feat(sbp_payments): fixtures and dio excp type added 2026-07-08 12:20:11 +03:00
Anatoliy Poloz
6d920857f6 fix(gradle) 2026-07-08 11:02:58 +03:00
Anatoliy
7183fc5d7d feat(sbp-payin): fixtures and banks assets added 2026-07-08 10:50:12 +03:00
Anatoliy
3852a06ab4 feat(payment): new sbp payment fixture added 2026-07-07 12:22:05 +03:00
misha
219908684f feat(video_calls): fixed naviagtion from push notification to call and added wake lock 2026-07-06 11:18:05 +03:00
misha
9ce7b035ff feat(video_calls): fixed call ended push handling 2026-06-30 18:48:45 +03:00
misha
c8ad600d97 fix: analyzer 2026-06-30 17:17:58 +03:00
misha
f493e07f9f feat(video_calls): call ended push, recall button for message 2026-06-30 16:11:18 +03:00
misha
c0c7e2e658 version changed 2026-06-29 12:15:34 +03:00
misha
f913ae60fa feat(vide_calls): added tokens clearing 2026-06-29 12:12:53 +03:00
misha
2100e5ca02 fix: analyzer fix 2026-06-26 11:30:49 +03:00
misha
10b36117a6 version changed 2026-06-26 08:57:31 +03:00
misha
bde4eea7d1 feat(video_calls): fixed recinnecting logic, added consultant bage 2026-06-26 08:56:57 +03:00
misha
59f42c9b1e fix: test fixed 2026-06-24 09:35:04 +03:00
misha
c5bf5acfe7 feat(video_calls): fixed online indicator and in app notifications for video call 2026-06-24 08:26:16 +03:00
7db9af9894 merge upstream 2026-06-23 08:56:24 +00:00
misha
8ff826fa3e fix: analyzer fix 2026-06-23 09:08:04 +03:00
misha
1e29cda65c Version changed 2026-06-23 08:43:40 +03:00
misha
ed72ed9799 feat(video_calls): changed accept flow, fixed ui bugs for PIP and video render sizes 2026-06-23 08:42:55 +03:00
misha
8920fc280f feat(ci/cd): version changed, anroind action runs only on main branch 2026-06-18 15:02:53 +03:00
misha
7493870a3d feat(video_calls): removed system ui, fixed wrong app initializing 2026-06-18 15:01:21 +03:00
misha
e3c22f8ac8 Revert "feat(video_calls): added system ui for call and navigation to the app"
This reverts commit ea5fced456.
2026-06-18 14:41:56 +03:00
misha
ea5fced456 feat(video_calls): added system ui for call and navigation to the app 2026-06-17 16:47:51 +03:00
misha
e3dd8162fa fix: ci/cd error fixed 2026-06-17 11:33:17 +03:00
misha
90e5c7e58d fix: ci/cd error fixed 2026-06-17 11:17:37 +03:00
misha
65d529b0af feat(video_calls): fixed navigation from voip push 2026-06-17 11:06:22 +03:00
misha
95c1d40c79 feat(video_calls): fixed nullable overlay state handling 2026-06-16 22:57:17 +03:00
misha
895443e747 feat(video_calls): added more logs for sentry 2026-06-16 21:11:45 +03:00
misha
367753d409 Version change 2026-06-16 19:30:01 +03:00
misha
e82866f936 feat(video_calls): voip push handling fix 2026-06-16 19:28:16 +03:00
misha
b13ff74220 fix: added NSLocationAlwaysAndWhenInUseUsageDescription key description 2026-06-16 18:33:21 +03:00
misha
e0d697e8d0 feat(video_calls): fixed ios push UI 2026-06-16 18:30:16 +03:00
misha
705538ac85 fix: added NSLocationWhenInUseUsageDescription key description 2026-06-16 17:56:59 +03:00
misha
03f037518e feat(video_calls): fixed ios and HMS pushes for calls 2026-06-16 15:01:46 +03:00
misha
a07565fca9 feat(ci/cd): removed pods updating 2026-06-15 16:47:17 +03:00
misha
b9cb4eeb84 feat(video_calls): app delegate fix 2026-06-15 16:27:24 +03:00
misha
4d531036fa Version changed 2026-06-15 14:12:37 +03:00
misha
1cafb4b05a feat(video_calls): changed chat button layoute 2026-06-15 14:07:56 +03:00
misha
5daa187045 feat(ci/cd): fixed app delegate for ios 2026-06-11 12:14:42 +03:00
misha
f4f884484d feat(ci/cd): separeted code sign from build_app 2026-06-11 11:55:43 +03:00
misha
4dd32e1a42 feat(ci/cd): fixed team id 2026-06-10 17:21:40 +03:00
misha
3e89bb5ca9 feat(ci/cd): added team id to build_app for ios 2026-06-10 17:14:15 +03:00
misha
885c6f66f9 feat(ci/cd): added gitea token for fastlane url 2026-06-10 16:39:06 +03:00
misha
9dc568ceab feat(ci/cd): removed flutter updating 2026-06-10 14:35:20 +03:00
misha
6fb151ef32 feat(ci/cd): added pods updating 2026-06-10 14:19:21 +03:00
misha
5fa024a7a8 feat(ci/cd): changed repositories for ci/cd 2026-06-10 14:09:54 +03:00
misha
de795c5274 feat(video_calls): added system android pip for calls 2026-06-10 09:32:43 +03:00
misha
27fe4afd81 feat(video_calls): added new incomng push impl, fixed mic and video sharing while connecting to call 2026-06-09 09:55:20 +03:00
misha
8e2b7147d5 Merge branch 'issue-975' of ssh://10.55.5.123:222/misha/eatvio_app into issue-975 2026-06-04 14:39:30 +03:00
misha
f51a68324b feat(video_calls): ios voip pushes handling 2026-06-04 14:38:27 +03:00
8a0d51add9 merge upstream 2026-06-02 11:43:59 +00:00
misha
634d83fd61 feat(tests): fixed tests 2026-05-28 12:11:29 +03:00
misha
9e9f2a9f45 feat(analyzer): fixed analyzer issue 2026-05-28 10:56:07 +03:00
misha
a495f98f30 feat(ci/cd): api key for matching 2026-05-27 16:32:33 +03:00
misha
9f8f9a7aaa feat(ci/cd): location fixes 2026-05-27 13:13:42 +03:00
misha
af1980930b feat(ci/cd): pods 403 error fix 2026-05-27 13:11:09 +03:00
misha
1c262da81a feat(ci/cd): make fstlane to update the certificates and profiles 2026-05-27 13:03:34 +03:00
misha
8a617509de version changed 2026-05-25 08:10:10 +03:00
misha
73cec1a01e feat(vide_calls): gms calls handling 2026-05-25 08:07:20 +03:00
fb99a42115 Merge pull request 'feat(video_calls)' (#980) from misha/eatvio_app:issue-974 into main
Reviewed-on: eatvio/eatvio_app#980
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-05-25 02:22:48 +00:00
misha
1acb158f36 feat(vidoe_calls): global cideo call state, connection, loading and error screens for video calls 2026-05-12 07:47:41 +03:00
misha
54c27c28cd feat(test): updated tests to increase tests coverage 2026-05-06 14:12:34 +03:00
misha
fe243965e3 feat(test): 70% tests coverage 2026-05-06 05:27:25 +03:00
misha
56313a4fcc Merge branch 'issue-974' of ssh://10.55.5.123:222/misha/eatvio_app into issue-974 2026-05-05 10:52:00 +03:00
misha
cb36ef8e07 Merge remote-tracking branch 'upstream/main' into issue-974 2026-05-05 10:51:20 +03:00
misha
b2072ab626 feat(video_calls): fixed falling test 2026-05-05 10:40:11 +03:00
5b72684c95 merge upstream 2026-05-05 05:17:22 +00:00
misha
91ad51d340 feat(video_calls): first impl for main video calls screen 2026-05-05 08:09:24 +03:00
01282a35cb merge upstream 2026-04-19 17:51:49 +00:00
5d86b7c71b Merge pull request 'version changed' (#971) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#971
2026-04-17 14:17:46 +00:00
misha
50aefde842 version changed 2026-04-17 17:17:04 +03:00
0991ac555e merge upstream 2026-04-17 14:13:58 +00:00
5fbd578463 Merge pull request 'ci/cd: fastlane version' (#970) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#970
2026-04-17 11:13:08 +00:00
misha
426bd49aa1 ci/cd: fastlane version 2026-04-17 14:12:10 +03:00
c6fc20d217 Merge pull request 'version changed' (#968) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#968
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2026-04-17 08:30:02 +00:00
misha
17e18dc68b version changed 2026-04-17 11:08:18 +03:00
b5a889cbb0 merge upstream 2026-04-16 17:22:23 +00:00
d483295aab Merge pull request 'feat(pushNotifications): fixed push notifications navigation' (#948) from misha/eatvio_app:push-notifications-navigation into main
Reviewed-on: eatvio/eatvio_app#948
Reviewed-by: Анатолий Полоз <t0ly.od@ya.ru>
2026-04-16 09:38:30 +00:00
misha
c8284338aa feat(image_compress_utility): adde test for corupted images 2026-04-15 16:27:27 +03:00
misha
03ec91a595 feat(image_compress_utility): improved compressing logic 2026-04-15 15:50:09 +03:00
misha
e814683984 version changed 2026-04-14 09:44:12 +03:00
misha
684415ef5b feat(setting): added profile data apdating for sttings screen, added validation for general info form 2026-04-14 09:28:02 +03:00
misha
631ceb2b7a version changed 2026-04-08 12:47:07 +03:00
misha
71a405c63f feat(notifications): changed navigation flow from Cancelled diet and started diet notifications 2026-04-08 12:43:15 +03:00
misha
4ca90eb2df feat(push_navigation): fixed navigation to chat screen 2026-04-08 12:14:43 +03:00
misha
f17f1ecb57 version changed 2026-04-07 10:32:10 +03:00
misha
a71bcc752d feat(fixes): fixed analyzer issue 2026-04-07 10:06:04 +03:00
misha
aac085def4 feat(app): refactoring of sentry using, feat(become_consultant): fixed possible infinite loading, feat(push_notifications): fixed push navigation from paid plan push 2026-04-07 10:05:11 +03:00
985154c863 merge upstream 2026-04-01 16:15:21 +00:00
misha
b6b13b1db5 version update 2026-04-01 13:21:19 +03:00
misha
6629286bf1 feat(CI/CD): changed gitea user token name 2026-04-01 10:59:54 +03:00
misha
966bc23f0b feat(CI/CD): removed proxy server 2026-04-01 10:49:50 +03:00
misha
a13b623ca2 feat(feed): fixed comments state updating in feed, feat(consultantRegistration): fixed registration flow for users with unverified phone, feat(tests): fixed tests coverage 2026-04-01 10:37:59 +03:00
misha
ee2dd75a94 feat(ci/cd): added test coverage verification 2026-03-31 10:20:43 +03:00
misha
c450a41715 feat(ci/cd): added token for repo 2026-03-30 10:33:46 +03:00
misha
55b6b75134 feat(ci/cd): env repo changed 2026-03-30 10:16:13 +03:00
misha
335e36dd18 feat(pushNotifications): fixed push notifications navigation 2026-03-26 18:06:20 +03:00
7106bde4ae Merge pull request 'fix 939, 927' (#940) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#940
Reviewed-by: misha <misha@noreply.localhost>
2026-03-26 08:58:17 +00:00
mso7nueh
4f241f7db6 feat: try push handling 2026-03-18 15:39:13 +03:00
mso7nueh
3d7755a9a2 feat: Introduce AppRouter for centralized application routing and deep link handling, and create push_navigation_handler for push notification navigation. 2026-03-17 16:33:54 +03:00
mso7nueh
decc8c63e6 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:32:31 +03:00
mso7nueh
ad5b48ef1b Fix merge conflicts with upstream main by retaining list refresh logic 2026-03-17 12:32:02 +03:00
mso7nueh
e8dc0138e2 Merge pull request 'feat(recipes, diets)' (#938) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#938
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-17 09:19:25 +00:00
mso7nueh
96896671b6 merge upstream 2026-03-17 09:18:48 +00:00
mso7nueh
5e2f0430d9 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-03-17 12:18:25 +03:00
mso7nueh
2da3b10e63 feat: Implement 'My Recipes' feature, add diary moderation request modal, and introduce diary repository. 2026-03-17 12:18:17 +03:00
misha
5cb121d733 fat(recipes, diets): added list refreshing 2026-03-16 10:32:12 +03:00
f1e07b3d86 merge upstream 2026-03-12 14:13:03 +00:00
mso7nueh
109013c25c Merge pull request 'feat(diary)' (#931) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#931
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-03-12 13:49:32 +00:00
misha
f5d14d213f Merge remote-tracking branch 'upstream/main' 2026-03-12 16:04:58 +03:00
misha
26ae28a705 feat(tests): fixed test errors 2026-03-12 14:41:16 +03:00
mso7nueh
7672477c03 Merge pull request 'Обновить pubspec.yaml' (#932) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#932
2026-03-11 12:15:49 +00:00
mso7nueh
07277d6e48 Обновить pubspec.yaml 2026-03-11 12:15:33 +00:00
misha
9d1bedff26 feat(diary): fixed consultant portion comments and day finishing errors handling 2026-03-11 11:55:10 +03:00
062079e7f1 merge upstream 2026-03-10 04:16:36 +00:00
misha
e39638be7f feat(notifications): removed unknown notification from layoute 2026-03-05 13:44:33 +03:00
442fc0ec14 Merge pull request 'feat(consultant_registration)' (#924) from misha/eatvio_app:issue-914 into main
Reviewed-on: eatvio/eatvio_app#924
2026-03-04 07:56:21 +00:00
a8f8cc8eb0 Merge pull request 'feat(notifications)' (#921) from misha/eatvio_app:issue-911 into main
Reviewed-on: eatvio/eatvio_app#921
2026-03-04 07:55:58 +00:00
misha
3892bf00aa fix: analizer 2026-03-04 09:25:11 +03:00
misha
a492c2d964 Merge remote-tracking branch 'upstream/main' into issue-914 2026-03-04 09:21:45 +03:00
misha
89268e1132 Merge remote-tracking branch 'upstream/main' into issue-911 2026-03-04 09:08:17 +03:00
60753a13d2 Merge pull request 'Рецепты и мелкие правки' (#920) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#920
2026-03-04 06:07:24 +00:00
misha
08eeea6ba4 feat(notifications): added push notifications navigation handling and fixed ws notification parsing 2026-03-04 09:05:00 +03:00
misha
58d99424a0 Merge remote-tracking branch 'upstream/main' into issue-911 2026-02-27 13:15:09 +03:00
misha
c6cdb5bf01 Merge branch 'main' of ssh://10.55.5.123:222/misha/eatvio_app into issue-911 2026-02-27 13:14:47 +03:00
mso7nueh
50deaeddd1 feat: Add Android CI workflow for testing and building APKs 2026-02-27 12:23:32 +03:00
mso7nueh
56fb2f1bac feat: Add Android CI workflow for testing and building APKs 2026-02-27 12:10:43 +03:00
mso7nueh
24185cc875 feat: Implement Gitea Actions CI/CD workflows for Android app testing, building, and deployment. 2026-02-26 16:34:45 +03:00
mso7nueh
9e7bf258d0 feat: Add Gitea workflow for Android app testing and APK build. 2026-02-26 16:18:56 +03:00
mso7nueh
098633b3ac Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-02-26 16:15:32 +03:00
mso7nueh
c3aa77b02d feat: Add GitHub Actions workflow for Android app testing, coverage analysis, and release APK building. 2026-02-26 16:14:55 +03:00
Username1548
2d5384f2ec fix: ci/cd 2026-02-26 14:10:41 +03:00
Username1548
1c7e494f90 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-02-26 14:00:52 +03:00
Username1548
8bbe2613d2 fix: ci/cd 2026-02-26 14:00:14 +03:00
mso7nueh
5f6ceab6a1 feat: Add Gitea CI/CD workflows for Android build, test, and coverage analysis. 2026-02-26 13:59:19 +03:00
Username1548
250a3f5d73 fix: CI/CD 2026-02-26 13:46:16 +03:00
mso7nueh
3e0d8b6c27 feat: Add GitHub Actions workflow for Android app testing and APK build. 2026-02-26 13:15:55 +03:00
mso7nueh
9ed3b37272 feat: Add Gitea Actions workflow for Flutter Android testing and building with coverage analysis. 2026-02-26 13:12:07 +03:00
mso7nueh
ae628a6ecc feat: Add Gitea workflows for Android APK build and comprehensive testing. 2026-02-26 13:04:20 +03:00
mso7nueh
88d8ec52d9 feat: add Android CI workflow for testing, coverage analysis, and APK build 2026-02-26 13:02:26 +03:00
mso7nueh
937921e26f feat: Add Gitea Actions workflow for Android app testing, building, and artifact upload. 2026-02-26 12:27:14 +03:00
mso7nueh
d324bd6540 feat: Add Gitea workflow files for Android testing and building. 2026-02-26 11:57:29 +03:00
mso7nueh
5766e216c8 feat: Add Gitea CI/CD workflows for Android app building and comprehensive testing with coverage. 2026-02-26 11:38:59 +03:00
mso7nueh
ee9b97b694 feat: Add Gitea Actions workflow for Flutter Android testing and release APK building. 2026-02-26 11:35:23 +03:00
mso7nueh
31eec6c9d7 feat: Implement Gitea Actions CI/CD workflows for Flutter Android app testing, building, and TestFlight deployment. 2026-02-26 11:21:48 +03:00
mso7nueh
5fae3eb5e8 feat: Add CI/CD workflows for building, testing, and deploying Android and iOS applications. 2026-02-26 11:18:51 +03:00
mso7nueh
b0a167b9e9 feat: Add MyRecipesScreen to display user's recipes, manage active/drafted states, and navigate to recipe creation. 2026-02-26 03:00:32 +03:00
mso7nueh
0b2e026d1d feat: Introduce user-generated content features, including recipe creation and management, and blog post creation. 2026-02-26 02:52:08 +03:00
mso7nueh
0641513ebe feat: Add user recipe management and creation functionality, including models, widgets, cubits, repositories, and tests. 2026-02-25 13:10:07 +03:00
misha
be0a99b164 fix: ci/cd 2026-02-25 13:02:28 +03:00
misha
8b1d08b2aa fix: ci/cd 2026-02-25 13:00:38 +03:00
misha
6dab0d159b feat(consultant_registration): added new consultant registration flow 2026-02-25 07:08:05 +03:00
24652884ee merge upstream 2026-02-24 11:02:11 +00:00
mso7nueh
5167766dbf merge upstream 2026-02-24 09:22:15 +00:00
cc79e40df9 Merge pull request 'feat(my_diets): draft diets fixes' (#901) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#901
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-02-24 07:52:31 +00:00
misha
cdc5936d3c analizer: fixed analizer issues 2026-02-20 13:22:16 +03:00
misha
67e770f5d0 review: fixed review issues 2026-02-20 13:20:17 +03:00
misha
6f666a8859 feat(tests): fixed notifications tests 2026-02-18 20:19:23 +03:00
misha
c540cbe6df feat(notifications): added navigation from different types of notifications 2026-02-18 19:45:32 +03:00
mso7nueh
6c4104e9ea feat: Issue-919 2026-02-18 18:08:00 +03:00
mso7nueh
6d1bcb85fb feat(plans): Issue-918 2026-02-18 18:05:59 +03:00
mso7nueh
9753644570 feat(recipe): drafts 2026-02-18 18:04:01 +03:00
mso7nueh
75983818d1 chore: Remove temporary test and analysis output files. 2026-02-16 18:20:00 +03:00
mso7nueh
5b149e2fd2 refactor: Corrected typo in MyRecipesCubit filename and updated related tests, widgets, services, and DI registrations. 2026-02-16 18:16:26 +03:00
misha
699148b202 feat(my_diets): fixed navigation to diet creation screen 2026-02-16 15:33:15 +03:00
mso7nueh
6e7894b911 build: Remove eatvio/ from .gitignore. 2026-02-16 12:49:09 +03:00
mso7nueh
d4f41fe6cf feat: implement recipe drafting functionality and refactor MyRecipesTab to be stateless 2026-02-16 12:49:09 +03:00
mso7nueh
bb97c395dd feat: Implement comprehensive navigation logic for various notification types within the NotificationWidget. 2026-02-12 15:55:11 +03:00
ea9cdb93d4 merge upstream 2026-02-11 17:26:07 +00:00
misha
b7804db49d feat(my_diets): merge conflict fix 2026-02-11 16:54:08 +03:00
misha
e90a466a5b Merge remote-tracking branch 'upstream/main' 2026-02-11 16:26:08 +03:00
misha
5f30aaeb2a feat(my_diets): merge conflict fixes 2026-02-11 16:23:48 +03:00
mso7nueh
516dfa4158 Merge pull request 'Улучшение валидации и пользовательского опыта в регистрациях и создании диет' (#910) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#910
Reviewed-by: misha <misha@noreply.localhost>
2026-02-11 09:45:19 +00:00
mso7nueh
ae07851396 test: update OAuth verification mock to use anyNamed arguments for email and phone. 2026-02-10 18:47:53 +03:00
mso7nueh
34eccb7db5 feat: Enhance OAuth error state to include specific registration validation errors and pass email in the verification request. 2026-02-10 18:36:18 +03:00
misha
042cc0ea20 Merge remote-tracking branch 'upstream/main' 2026-02-10 12:38:08 +03:00
misha
f81aebe9d8 feat(tests): added new tests and updated fixtures 2026-02-10 08:02:50 +03:00
mso7nueh
d70aaf955f feat: Implement auto-scrolling to the first validation error in the create diet form and refine image validation logic. 2026-02-09 23:15:12 +03:00
misha
c91e83adaf feat(my_diets): fixed draft, active and personal diets creation, updating 2026-02-09 10:39:51 +03:00
mso7nueh
531f732b1a Merge pull request 'feat: enhance BottomNavigationBarWidget with DEV logo display and refactor page list initialization' (#907) from mso7nueh/eatvio_app:feat_add_dev_marker_logo into main
Reviewed-on: eatvio/eatvio_app#907
Reviewed-by: misha <misha@noreply.localhost>
2026-02-06 09:54:34 +00:00
mso7nueh
d10f2f1356 Merge pull request 'feat: update add product modal to reflect selection state with icons' (#906) from mso7nueh/eatvio_app:feat_add_product_modal into main
Reviewed-on: eatvio/eatvio_app#906
Reviewed-by: misha <misha@noreply.localhost>
2026-02-06 09:54:25 +00:00
7c4010b275 merge upstream 2026-02-06 07:09:12 +00:00
mso7nueh
bf110b70c1 feat: enhance BottomNavigationBarWidget with DEV logo display and refactor page list initialization 2026-02-05 14:41:25 +03:00
mso7nueh
97418e11d4 feat: update add product modal to reflect selection state with icons 2026-02-05 14:40:55 +03:00
mso7nueh
225077907f Merge pull request 'build: bump version to 1.3.17+97' (#903) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#903
2026-02-04 12:50:52 +00:00
mso7nueh
296adc8229 build: bump version to 1.3.17+97 2026-02-04 15:50:28 +03:00
mso7nueh
7dd7e3b488 Merge pull request 'feat: Integrate Huawei Mobile Services (HMS) by updating configuration, adding manifest metadata, and differentiating Huawei devices for token submission.' (#902) from mso7nueh/eatvio_app:fix_hms_push_token into main
Reviewed-on: eatvio/eatvio_app#902
Reviewed-by: misha <misha@noreply.localhost>
2026-02-04 12:49:13 +00:00
mso7nueh
a496edaf4c feat: Integrate Huawei Mobile Services (HMS) by updating configuration, adding manifest metadata, and differentiating Huawei devices for token submission. 2026-02-04 13:23:58 +03:00
misha
400cc004ef fix: analizer fixed 2026-02-04 12:21:09 +03:00
misha
beda152df0 fix: temporary removed tests from ci/cd 2026-02-04 11:46:47 +03:00
misha
42ffde820d feat(my_diets): draft diets fixes 2026-02-04 11:38:53 +03:00
4458aaf74f merge upstream 2026-02-04 07:52:24 +00:00
mso7nueh
adb7b0bd74 Merge pull request 'chore: update project version' (#898) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#898
2026-02-03 12:12:43 +00:00
mso7nueh
a768eefed0 chore: update project version 2026-02-03 15:12:07 +03:00
mso7nueh
0ac50d6d18 Merge pull request 'feat: Add local Mulish font family and remove Google Fonts dependency.' (#897) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#897
2026-02-03 11:50:27 +00:00
mso7nueh
a0d8fda50d feat: Add local Mulish font family and remove Google Fonts dependency. 2026-02-03 14:49:49 +03:00
mso7nueh
1d286fa8d9 Merge pull request 'feat: introduce My Diets and Create Diet features with associated UI, state management, and data handling.' (#889) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#889
Reviewed-by: misha <misha@noreply.localhost>
2026-02-03 08:16:56 +00:00
mso7nueh
c2a5e7305e feat: add a customizable FeedIconButton widget that supports text, IconData, or SvgPicture. 2026-02-02 15:32:10 +03:00
mso7nueh
bbb1b28692 merge upstream 2026-02-02 12:12:12 +00:00
mso7nueh
21a65caea9 Обновить .gitea/workflows/build.yml 2026-02-02 12:09:13 +00:00
mso7nueh
eac366e9ae Обновить .gitea/workflows/test.yml 2026-02-02 12:09:00 +00:00
mso7nueh
278f4d7e10 feat: Add numerous repository implementations, cubits, and core utilities across various features, and update dependencies. 2026-02-02 14:24:17 +03:00
mso7nueh
ac5504a514 feat: Implement FeedScreen with dynamic content display, search functionality, and filtering options. 2026-01-30 14:05:45 +03:00
mso7nueh
843e1ce4dd test: add unit tests for ValidationUtils covering email, phone, URL, password, username, text, title, and description length validations, along with phone normalization and error message generation. 2026-01-29 16:01:18 +03:00
mso7nueh
db6db89567 feat: add unit tests for TokenInterceptor's request and error handling logic. 2026-01-29 15:57:11 +03:00
mso7nueh
044d86e710 fix mask formatter in phone 2026-01-29 15:49:13 +03:00
mso7nueh
4ff880e079 feat: add register via social screen 2026-01-29 15:42:02 +03:00
mso7nueh
05b9ca17c7 feat: Introduce token interceptor for authentication, WebSocket manager for real-time updates, and a new menu screen. 2026-01-29 15:00:04 +03:00
f20f871c88 merge upstream 2026-01-28 15:45:54 +00:00
mso7nueh
a1c0c2694b test: Add basic widget tests for MyDietsTab and MyDietWidget, and integration tests for create_diet. 2026-01-28 18:16:24 +03:00
mso7nueh
2d7407ce99 feat: introduce My Diets and Create Diet features with associated UI, state management, and data handling. 2026-01-28 17:32:29 +03:00
62fe5ebfa0 Merge pull request 'feat(consultant)' (#888) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#888
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-01-28 13:31:42 +00:00
misha
3dc36ab52d feat(consultants): fixed infinite loading after verifying phone 2026-01-28 12:56:04 +03:00
190e212eb4 Merge pull request 'feat(tests)' (#876) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#876
2026-01-28 09:44:50 +00:00
misha
cf3d1a945c Merge remote-tracking branch 'upstream/main' 2026-01-28 11:52:38 +03:00
mso7nueh
c54915f45d Обновить .gitea/workflows/test-flight-cd.yaml 2026-01-27 15:17:51 +00:00
mso7nueh
b188570882 Обновить .gitea/workflows/test-flight-cd.yaml 2026-01-27 15:14:00 +00:00
mso7nueh
d0794670f6 Обновить .gitea/workflows/test-flight-cd.yaml 2026-01-27 15:07:38 +00:00
mso7nueh
3b53ed6f7d Merge pull request 'Some fixes' (#873) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#873
Reviewed-by: misha <misha@noreply.localhost>
2026-01-27 14:57:23 +00:00
mso7nueh
a003814265 chore: update Huawei dependencies in build.gradle to latest versions for improved functionality 2026-01-27 16:56:22 +03:00
mso7nueh
e99465a550 chore: add kotlin-parcelize plugin to build.gradle for improved parceling support 2026-01-27 16:29:17 +03:00
mso7nueh
a01f3914c4 fix: update file_picker dependency version format in pubspec.yaml 2026-01-27 15:50:11 +03:00
mso7nueh
15f9149cfd feat: Introduce Gitea workflows for Android testing, coverage analysis, and build environment setup. 2026-01-27 15:14:38 +03:00
mso7nueh
79df8c5f9a feat: introduce DiaryCubit for managing user diary and diet plans, and add new unit tests for various features. 2026-01-27 14:33:21 +03:00
mso7nueh
a93da1b505 Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2026-01-27 13:44:39 +03:00
mso7nueh
8144013747 feat: diary add modal and error dialogs 2026-01-27 13:44:35 +03:00
misha
7bac2a13ac Merge remote-tracking branch 'upstream/main' 2026-01-26 16:55:08 +03:00
misha
29589ef6fd feat(tests): test coverage is 70.5 2026-01-26 16:38:15 +03:00
mso7nueh
97a6fe57e9 merge upstream 2026-01-26 12:30:55 +00:00
mso7nueh
8309498d9e feat: Increment app version and add tests for glossary moderation request modal widget. 2026-01-26 14:51:22 +03:00
mso7nueh
aa098b31bc feat: add glossary moderation request modal widget to handle new product submissions. 2026-01-26 14:42:43 +03:00
mso7nueh
7a85c65918 feat: add smoke tests for the DietPreviewScreen to verify rendering and information display 2026-01-26 14:39:24 +03:00
mso7nueh
6ff35fd162 feat: Implement DietPreviewScreen to display comprehensive diet details including schedule and nutritional information. 2026-01-26 14:34:38 +03:00
mso7nueh
6811b325be feat: Implement core authentication logic with AuthCubit and introduce a comprehensive email verification flow. 2026-01-26 14:31:39 +03:00
misha
92205c09d6 feat(tests): test coverage is 70.4 2026-01-23 12:40:17 +03:00
008dba9fed Merge pull request 'feat(tests)' (#867) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#867
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-01-23 09:20:37 +00:00
f61fb8c6b3 merge upstream 2026-01-23 07:40:43 +00:00
mso7nueh
55ca558bfb fix: analyzer 2026-01-22 12:26:57 +03:00
mso7nueh
91499ae207 feat: implement diary repository and cubit for managing daily food entries, nutritional data, and user interactions. 2026-01-22 00:58:56 +03:00
mso7nueh
4bacdf5e76 feat: implement UI for managing recipe ingredients with glossary search, nutritional calculation, and moderation request functionality. 2026-01-22 00:50:20 +03:00
mso7nueh
e76c1aed20 feat: implement recipe creation feature including state management, ingredient/step management, image handling, and validation. 2026-01-22 00:42:30 +03:00
mso7nueh
de8f879f59 feat: Implement social authentication facade for VK, OK.ru, Mail.ru, and Yandex. 2026-01-21 23:54:08 +03:00
misha
f6809a06b3 feat(tests): test coverage is 69.2 2026-01-21 09:19:41 +03:00
misha
775d4fb80f Merge remote-tracking branch 'upstream/main' 2026-01-20 12:00:44 +03:00
mso7nueh
64e9df69f7 Merge pull request 'feat: Implement glossary moderation, notifications, product modals, social authentication, and internationalization.' (#868) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#868
Reviewed-by: misha <misha@noreply.localhost>
2026-01-19 18:46:05 +00:00
3d14dee4e9 merge upstream 2026-01-19 13:07:28 +00:00
mso7nueh
6ccfcdedda feat: create AddProductModal for adding and editing diary product portions with glossary search functionality. 2026-01-19 16:06:19 +03:00
mso7nueh
d3c8034b8f feat: Implement glossary moderation, notifications, product modals, social authentication, and internationalization. 2026-01-19 16:00:30 +03:00
misha
44ede73ceb feat(tests): tests coverage 67.2 2026-01-19 12:32:11 +03:00
2c63990afc Merge pull request 'feat(tests)' (#864) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#864
Reviewed-by: mso7nueh <mso7nueh@mail.ru>
2026-01-19 09:12:46 +00:00
38647b4ac6 merge upstream 2026-01-15 22:01:54 +00:00
mso7nueh
eb69b253bc Merge pull request 'chore: enhance markdown processing and UI adjustments in comments input' (#852) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#852
Reviewed-by: misha <misha@noreply.localhost>
2026-01-15 10:28:12 +00:00
mso7nueh
f26886a7c5 Merge pull request 'fix: splash screen stuck' (#861) from mso7nueh/eatvio_app:fix_splash_stuck into main
Reviewed-on: eatvio/eatvio_app#861
Reviewed-by: misha <misha@noreply.localhost>
2026-01-15 10:27:57 +00:00
misha
13ccf56df0 feat(tests): updated tests coverage to 66.5% 2026-01-14 18:47:11 +03:00
mso7nueh
c0032f7a95 feat: enhance Firebase messaging error logging and simplify BottomNavigationBarWidget by removing profileModel dependency 2026-01-14 16:09:07 +03:00
mso7nueh
4852d2cf10 refactor: replace HTML rendering with MarkdownHtmlWidget across various components for consistent markdown processing 2026-01-14 14:48:32 +03:00
mso7nueh
41e96c63c6 feat(token_storage): add clearCache method for testing and ensure fresh state in tests 2026-01-14 13:02:24 +03:00
mso7nueh
c786821275 fix: handle null context in MarkdownHtmlWidget and improve test stability with pumpAndSettle calls 2026-01-13 13:03:58 +03:00
mso7nueh
7b425e56f1 chore: remove unused markdown_utils import from expandable_text.dart 2026-01-13 12:37:10 +03:00
mso7nueh
bf2ba1fbda feat: integrate MarkdownHtmlWidget for improved markdown rendering in blog and diet components 2026-01-13 12:36:12 +03:00
c836b600c6 merge upstream 2026-01-13 09:25:36 +00:00
mso7nueh
d206e7ec12 refactor: implement lazy initialization for global BlocProviders and enhance Firebase initialization with timeout handling 2026-01-13 12:22:22 +03:00
mso7nueh
a778257215 chore: enhance markdown processing and UI adjustments in comments input 2026-01-12 16:31:22 +03:00
mso7nueh
497ff19cfa Merge pull request 'fix(registration): update navigation to verify email screen and streamline error message display' (#845) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#845
2025-12-26 13:26:04 +00:00
mso7nueh
bb59552e3e fix(registration): update navigation to verify email screen and streamline error message display 2025-12-26 16:17:34 +03:00
mso7nueh
55b2bb0436 Merge pull request 'refactor(tests): update password and sex widget tests to use const constructors for RegistrationStateV2' (#841) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#841
2025-12-26 10:54:42 +00:00
mso7nueh
90746d0be7 refactor(tests): update password and sex widget tests to use const constructors for RegistrationStateV2 2025-12-26 13:54:10 +03:00
mso7nueh
d70d172c69 chore: update version to 1.3.11+91 in pubspec.yaml; clean up whitespace in various files 2025-12-26 13:51:44 +03:00
mso7nueh
4e23992d22 chore: bump version number to 1.3.11+91 in pubspec.yaml 2025-12-26 13:37:41 +03:00
mso7nueh
7a846ed218 chore: add blank lines for improved readability in multiple files 2025-12-26 13:35:08 +03:00
mso7nueh
fcc2936c22 Merge pull request 'feat(tests), feat(registration), feat(diary), feat(oAuth)' (#835) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#835
2025-12-25 14:19:38 +00:00
misha
b11cf2c0b0 Merge remote-tracking branch 'upstream/main' 2025-12-25 16:36:23 +03:00
misha
0ead14bca5 feat(registration): added email verification, feat(diary): fixed moderation request, feat(oauth): fixed email and phone verification 2025-12-25 16:31:52 +03:00
mso7nueh
08e56d9777 Merge pull request 'chore: update Flutter version in CI workflow from 3.38.1 to 3.38.5' (#831) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#831
2025-12-25 12:55:49 +00:00
misha
99659f7ea1 feat(tests): increased tests coverage 2025-12-25 12:56:38 +03:00
mso7nueh
e158d812e4 refactor: clean up imports and improve code readability across various files 2025-12-24 17:54:35 +03:00
mso7nueh
3d774267ca chore: update pubspec.yaml dependencies and Flutter SDK version; add blank lines in various files for improved readability 2025-12-24 14:52:51 +03:00
mso7nueh
aeb89a9aba merge upstream 2025-12-23 13:01:33 +00:00
mso7nueh
a4f5bfdb4d Merge pull request 'feat(tests)' (#832) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#832
2025-12-23 13:01:15 +00:00
misha
5b705dfb72 feat(tests): test coverage 60% 2025-12-23 10:59:56 +03:00
mso7nueh
a792dcd163 chore: update dependencies in pubspec.yaml for retrofit and development tools 2025-12-23 10:37:41 +03:00
mso7nueh
9c7ca14017 chore: update Flutter version in CI workflow from 3.38.1 to 3.38.5 2025-12-23 10:21:15 +03:00
mso7nueh
38118055a5 Merge pull request 'fix: ui overflow nav-bar, name files in chat, qr-code scanner in dev-tools, message field size, keyboard type' (#830) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#830
2025-12-22 13:53:25 +00:00
mso7nueh
5d6e25858b test(chat): update chat service test to validate message response and ensure correct message model handling 2025-12-22 16:52:40 +03:00
mso7nueh
42b443c3ba fix: ui overflow nav-bar, name files in chat, qr-code scanner in dev-tools, message field size, keyboard type 2025-12-22 16:48:13 +03:00
mso7nueh
21f7c40cde Merge pull request 'feat(chat): enhance chat message model and repository to support improved message handling and parsing' (#816) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#816
2025-12-19 12:36:00 +00:00
mso7nueh
f56092d88f feat(chat): enhance chat message model and repository to support improved message handling and parsing 2025-12-19 15:35:37 +03:00
mso7nueh
2b97c0f57d Merge pull request 'feat(router): enhance deep link handling with logging and support for various content types' (#815) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#815
2025-12-19 12:16:12 +00:00
mso7nueh
a1304e4612 feat(router): enhance deep link handling with logging and support for various content types 2025-12-19 15:15:54 +03:00
mso7nueh
7b534cf891 Merge pull request 'feat(chat): add plan order message widget and enhance chat bubble to support message types' (#814) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#814
2025-12-19 12:15:42 +00:00
mso7nueh
22b2e5f249 feat(chat): add plan order message widget and enhance chat bubble to support message types 2025-12-19 15:08:11 +03:00
mso7nueh
5dfdbe4543 Merge pull request 'feat(ios): read and set Flutter build number in Fastfile for app store deployment' (#813) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#813
2025-12-17 12:52:57 +00:00
mso7nueh
5c9a9c22ba feat(ios): read and set Flutter build number in Fastfile for app store deployment 2025-12-17 15:52:34 +03:00
mso7nueh
4f920359c8 Merge pull request 'implement OAuth verification request and confirmation flow with error handling and UI updates' (#811) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#811
2025-12-17 12:42:26 +00:00
mso7nueh
2ab3bc1cf0 feat(auth): implement OAuth verification request and confirmation flow with error handling and UI updates 2025-12-17 15:41:38 +03:00
mso7nueh
91bd8f9d68 merge upstream 2025-12-17 07:48:37 +00:00
9c6f6bdfb1 Merge pull request 'feat(diary): meal summary fix' (#810) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#810
2025-12-17 07:33:36 +00:00
misha
fe1e81e455 feat(diary): meal summary fix 2025-12-17 08:38:13 +03:00
mso7nueh
b7e064a560 test(tests): add whitespace to multiple test files for consistency and improved readability 2025-12-16 14:18:25 +03:00
mso7nueh
f1b7a24044 feat(coverage): enhance coverage analysis script to filter generated files and generate detailed markdown report for files with less than 100% coverage 2025-12-15 23:37:17 +03:00
dfe11a6df2 merge upstream 2025-12-12 09:55:45 +00:00
mso7nueh
cb441b4027 Merge branch 'mso7nueh-main' 2025-12-12 11:38:22 +03:00
7282fea59a Merge pull request 'changed version' (#807) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#807
2025-12-12 07:52:00 +00:00
misha
4adec4dbbc changed version 2025-12-12 10:50:37 +03:00
mso7nueh
d5b0e972a5 chore(version): update app version to 1.3.7+87 and adjust related iOS project settings 2025-12-12 10:43:26 +03:00
mso7nueh
391efeb8dd fix(chat): initialize startDate to current date when personal diet is not enabled and streamline widget layout for improved readability 2025-12-12 10:38:53 +03:00
d115af6f8e Merge pull request 'feat(tests)' (#805) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#805
2025-12-12 07:15:24 +00:00
mso7nueh
2408443a26 test(tests): add whitespace to multiple test files for consistency and improved readability 2025-12-11 21:43:36 +03:00
mso7nueh
c378c7fc16 test(tests): add extensive widget tests for MarkdownEditor and CommentsActionsBottomSheet, improving test coverage and functionality validation 2025-12-11 21:19:41 +03:00
misha
070a3e39e7 feat(tests): added services tests and fixtures 2025-12-11 15:53:35 +03:00
mso7nueh
d1753e99f8 Merge pull request 'tests' (#804) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#804
2025-12-11 11:13:28 +00:00
mso7nueh
960afbd6ed chore(dependencies): add webview_flutter_platform_interface package to pubspec.yaml and clean up test files by removing unnecessary whitespace 2025-12-11 14:12:41 +03:00
mso7nueh
70e4a42e98 test(tests): add comprehensive unit tests for various models and repositories, enhancing test coverage and ensuring correct functionality 2025-12-09 16:19:48 +03:00
mso7nueh
04061cb4c1 Merge pull request 'chore(version): update app version to 1.3.6+86 and filter active plans in PlansRepository' (#802) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#802
2025-12-08 09:11:29 +00:00
mso7nueh
edd064d20d chore(version): update app version to 1.3.6+86 and filter active plans in PlansRepository 2025-12-08 12:10:19 +03:00
mso7nueh
cdb7378265 Merge pull request 'chore(version): update app version to 1.3.5+85 and improve test coverage reporting in CI' (#801) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#801
2025-12-08 08:55:58 +00:00
mso7nueh
746d366c18 refactor(tests): replace 'final' with 'const' for improved performance in various test files 2025-12-08 11:32:40 +03:00
mso7nueh
c2a506cdbf chore(version): update app version to 1.3.5+85 and improve test coverage reporting in CI 2025-12-08 11:19:08 +03:00
mso7nueh
abe4985f0e Merge pull request 'main' (#800) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#800
2025-12-04 13:14:34 +00:00
mso7nueh
ca06b2bd0f chore(tests): remove unused tests and clean up test files for MyDietsRepository and MyPlansCubit 2025-12-04 16:13:36 +03:00
mso7nueh
04c932646d Merge resolve-conflicts into main 2025-12-04 16:13:30 +03:00
mso7nueh
3d4363185b Resolve merge conflicts: add network_image_mock to pubspec.yaml 2025-12-04 15:50:53 +03:00
mso7nueh
953230b387 feat(dependencies): add rxdart package and improve string extension methods for better handling of empty strings and gender parsing 2025-12-04 15:45:54 +03:00
mso7nueh
9d845e1264 chore(dependencies): add gpassword package to pubspec.yaml 2025-12-03 18:42:48 +03:00
mso7nueh
32de60742e Merge mso7nueh-main into main: resolve conflicts in pubspec.yaml and feed_repository_test.dart 2025-12-03 18:38:37 +03:00
mso7nueh
3923b61c67 Merge mso7nueh-main into main: resolve conflicts 2025-12-03 18:34:25 +03:00
mso7nueh
0cd8e662d9 feat(blog): enhance markdown processing in blog, diet, plan, recipe, and my diet widgets to auto-convert URLs to markdown links and improve link handling 2025-12-03 18:20:44 +03:00
89ea73fc59 Merge pull request 'feat(tests): widget and unit tests for chat' (#796) from misha/eatvio_app:issue-795 into main
Reviewed-on: eatvio/eatvio_app#796
2025-12-03 15:18:49 +00:00
misha
9e33063c30 feat(tests): widget and unit tests for chat 2025-12-03 18:12:17 +03:00
mso7nueh
f3d4478c65 refactor(tests): update model instantiation to use const for improved performance 2025-12-03 06:16:34 +03:00
673aa86370 Merge pull request 'feat(tests): widget and unit tests for diets, plans and recipes' (#794) from misha/eatvio_app:issue-793 into main
Reviewed-on: eatvio/eatvio_app#794
2025-12-02 12:15:07 +00:00
misha
1738d27b82 added widget and unit tests for plans, diets, recipes 2025-12-02 12:34:13 +03:00
mso7nueh
d0becb9e11 feat(test): enhance internet connection tests with dotenv integration and async handling 2025-12-01 23:54:41 +03:00
mso7nueh
965ae2f02b feat(test): update oauth handling and improve preferences management 2025-12-01 23:51:40 +03:00
mso7nueh
9fcfac87d4 feat(test): oauth, prefs, credentials 2025-11-28 14:22:05 +03:00
mso7nueh
2a25be5aa4 feat(test): comments, auth, feed 2025-11-28 14:13:05 +03:00
b420d11996 Merge pull request 'feat(tests)' (#792) from misha/eatvio_app:issue-791 into main
Reviewed-on: eatvio/eatvio_app#792
2025-11-28 07:55:06 +00:00
mso7nueh
7df32dabd2 feat(tests): menu, settings, remove icov, remove generated files from % tests 2025-11-27 08:18:23 +03:00
mso7nueh
2e57cd9519 feat(consultant): Issue-420 + Tests 2025-11-25 15:52:41 +03:00
mso7nueh
58bb766721 feat(tests): profile tests 2025-11-21 23:31:13 +03:00
mso7nueh
0be7392a39 feat(tests): add fixtures 2025-11-21 23:24:49 +03:00
35cad4e05e Merge pull request 'sync upsteam' (#3) from eatvio/eatvio_app:main into main
Reviewed-on: apoloz/eatvio_app#3
2025-11-20 17:24:29 +00:00
mso7nueh
4deda92c5e feat(tests): add fixtures 2025-11-20 16:58:56 +03:00
mso7nueh
6fa944b3df Merge branch 'main' of http://gitea.office.local:3000/mso7nueh/eatvio_app 2025-11-19 04:49:05 +03:00
mso7nueh
c31411c4a2 fix: analyzer 2025-11-19 04:48:35 +03:00
mso7nueh
6214d19a14 feat(auth): tests Issue-761 2025-11-19 04:47:30 +03:00
mso7nueh
db5dddff86 feat(barcode): add scanner, upd flutter version in ci 2025-11-17 20:25:52 +03:00
1070 changed files with 111295 additions and 7473 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -3,19 +3,19 @@ name: Eatvio App Android Test
on: [ pull_request ] on: [ pull_request ]
env: env:
flutter_version: "3.32.0" flutter_version: "3.41.2"
java_version: "21.x" java_version: "21.x"
flutter_channel: "stable" flutter_channel: "stable"
jobs: jobs:
build: build:
runs-on: [ dev-office ] runs-on: [ ubuntu-latest ]
steps: steps:
- uses: actions/checkout@v3 - uses: https://github.com/actions/checkout@v3
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v3 uses: https://github.com/actions/setup-java@v3
with: with:
distribution: "zulu" distribution: "zulu"
java-version: ${{ env.java_version }} java-version: ${{ env.java_version }}
@@ -24,7 +24,7 @@ jobs:
- name: Install Android SDK - name: Install Android SDK
run: | run: |
apt-get update apt-get update
apt-get install -y openjdk-11-jdk wget apt-get install -y openjdk-11-jdk wget xz-utils tar
wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip wget https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip -O commandlinetools.zip
unzip commandlinetools.zip unzip commandlinetools.zip
mkdir -p $HOME/Android/Sdk/cmdline-tools/latest mkdir -p $HOME/Android/Sdk/cmdline-tools/latest
@@ -44,7 +44,7 @@ jobs:
apt-get install -y jq apt-get install -y jq
- id: flutter-action - id: flutter-action
uses: actions/flutter-action@v2 uses: https://github.com/subosito/flutter-action@v2
with: with:
channel: ${{ env.flutter_channel }} channel: ${{ env.flutter_channel }}
flutter-version: ${{ env.flutter_version }} flutter-version: ${{ env.flutter_version }}
@@ -79,7 +79,7 @@ jobs:
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
- name: Upload APK artifact - name: Upload APK artifact
uses: actions/upload-artifact@v3 uses: https://github.com/actions/upload-artifact@v3
with: with:
name: ${{ env.APK_NAME }} name: ${{ env.APK_NAME }}
path: ${{ env.APK_NAME }} path: ${{ env.APK_NAME }}

View File

@@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker
on: [ pull_request ] on: [ pull_request ]
env: env:
flutter_version: "3.32.0" flutter_version: "3.41.2"
java_version: "21.x" java_version: "21.x"
flutter_channel: "stable" flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/" my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -15,7 +15,7 @@ jobs:
runs-on: [ dev-office-flutter ] runs-on: [ dev-office-flutter ]
steps: steps:
- uses: actions/checkout@v3 - uses: https://github.com/actions/checkout@v3
- name: Set up Android environment variables - name: Set up Android environment variables
id: "android-env-setup" id: "android-env-setup"
@@ -52,7 +52,7 @@ jobs:
# echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV # echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
# - name: Upload APK artifact # - name: Upload APK artifact
# uses: actions/upload-artifact@v3 # uses: https://github.com/actions/upload-artifact@v3
# with: # with:
# name: ${{ env.APK_NAME }} # name: ${{ env.APK_NAME }}
# path: ${{ env.APK_NAME }} # path: ${{ env.APK_NAME }}

View File

@@ -8,15 +8,19 @@ jobs:
build: build:
runs-on: [macos-arm64] runs-on: [macos-arm64]
steps: steps:
- name: Fastlane version
run: fastlane --version
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: https://github.com/actions/checkout@v2
- name: Checkout env repository - name: Checkout env repository
uses: actions/checkout@v2 uses: https://github.com/actions/checkout@v3
with: with:
repository: mso7nueh/eatvio_app_env repository: eatvio/eatvio_app_env
path: env_repo path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }} # Здесь может быть токен любого юзера с доступом в gitea
token: ${{ vars.READ_USER_GITEA_TOKEN }}
ref: main ref: main
- name: Copy .env file - name: Copy .env file
@@ -54,21 +58,20 @@ jobs:
- name: Flutter build - name: Flutter build
run: | run: |
flutter --version
flutter clean flutter clean
flutter pub get flutter pub get
flutter pub upgrade
flutter pub run build_runner build --delete-conflicting-outputs flutter pub run build_runner build --delete-conflicting-outputs
# Configure Flutter for release mode
flutter build ios --config-only --release flutter build ios --config-only --release
cd ios cd ios
pod update pod install
fastlane beta fastlane beta
env: env:
LANG: en_US.UTF-8 LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8 LC_ALL: en_US.UTF-8
MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }}
FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }}
MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane MATCH_GIT_URL: http://eatvio:${{ vars.READ_USER_GITEA_TOKEN }}@gitea.office.local:3000/eatvio/fastlane.git
FASTLANE_USER: eatvio2023@icloud.com FASTLANE_USER: eatvio2023@icloud.com
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }}

View File

@@ -1,27 +1,42 @@
name: Eatvio App Android Test name: Eatvio App Android Test
on: [ pull_request ] on:
pull_request:
branches:
- main
env: env:
flutter_version: "3.35.1" flutter_version: '3.41.2'
java_version: "21.x" java_version: '21.x'
flutter_channel: "stable" flutter_channel: 'stable'
my_local_cache_dir: "/opt/hostedtoolcache/" my_local_cache_dir: '/opt/hostedtoolcache/'
ANDROID_SDK_ROOT: "/opt/hostedtoolcache/Android/Sdk" ANDROID_SDK_ROOT: '/opt/hostedtoolcache/Android/Sdk'
jobs: jobs:
build: build:
runs-on: [ dev-office ] runs-on: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v3 - uses: https://github.com/actions/checkout@v3
# Пока что не нужно проксирование
# - name: Set proxy server
# env:
# PROXY_USER: ${{ secrets.PROXY_USER }}
# PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }}
# run: |
# echo "Setting up proxy..."
# cat <<EOF >/etc/apt/apt.conf.d/proxy.conf
# Acquire::http::Proxy "socks5h://${PROXY_USER}:${PROXY_PASSWORD}@10.55.5.202:19843/";
# EOF
- name: Checkout env repository - name: Checkout env repository
uses: actions/checkout@v3 uses: https://github.com/actions/checkout@v3
with: with:
repository: mso7nueh/eatvio_app_env repository: eatvio/eatvio_app_env
path: env_repo path: env_repo
token: ${{ vars.MSO7NUEH_TOKEN }} # Здесь может быть токен любого юзера с доступом в gitea
token: ${{ vars.READ_USER_GITEA_TOKEN }}
ref: main ref: main
- name: Copy .env file - name: Copy .env file
@@ -56,22 +71,22 @@ jobs:
- name: Cache Android SDK - name: Cache Android SDK
id: cache-sdk id: cache-sdk
uses: "https://gitea.com/actions/cache@v4" uses: 'https://github.com/actions/cache@v4'
with: with:
path: ${{ env.my_local_cache_dir }} path: ${{ env.my_local_cache_dir }}
key: ${{ runner.os }}-cache-sdk key: ${{ runner.os }}-cache-sdk
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@v3 uses: https://github.com/actions/setup-java@v3
with: with:
distribution: "zulu" distribution: 'zulu'
java-version: ${{ env.java_version }} java-version: ${{ env.java_version }}
cache: gradle cache: gradle
- name: Install utils for Android SDK - name: Install utils for Android SDK
run: | run: |
apt-get update apt-get update
apt-get install -y openjdk-11-jdk wget jq clang cmake ninja-build libgtk-3-dev apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
- name: Install Android SDK - name: Install Android SDK
if: steps.cache-sdk.outputs.cache-hit != 'true' if: steps.cache-sdk.outputs.cache-hit != 'true'
@@ -90,25 +105,31 @@ jobs:
sdkmanager "platform-tools" "platforms;android-35" "platforms;android-34" "platforms;android-33" "platforms;android-31" sdkmanager "platform-tools" "platforms;android-35" "platforms;android-34" "platforms;android-33" "platforms;android-31"
sdkmanager "build-tools;34.0.0" sdkmanager "build-tools;34.0.0"
sdkmanager "ndk;25.1.8937393" sdkmanager "ndk;25.1.8937393"
sdkmanager --install "cmake;3.22.1" sdkmanager --install "cmake;3.22.1"
- name: Set up Android environment variables - name: Set up Android environment variables
id: "android-env-setup" id: 'android-env-setup'
run: | run: |
echo "ANDROID_SDK_ROOT=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV echo "ANDROID_SDK_ROOT=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV echo "PATH=$PATH:${{ env.my_local_cache_dir }}Android/Sdk/cmdline-tools/latest/bin:${{ env.my_local_cache_dir }}Android/Sdk/platform-tools:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/30.0.3:${{ env.my_local_cache_dir }}Android/Sdk/build-tools/34.0.0" >> $GITHUB_ENV
echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV echo "ANDROID_HOME=${{ env.my_local_cache_dir }}Android/Sdk" >> $GITHUB_ENV
- id: flutter-action - name: Set up Flutter
uses: actions/flutter-action@v2 run: |
with: FLUTTER_DIR="${{ env.my_local_cache_dir }}flutter"
channel: ${{ env.flutter_channel }} if [ ! -f "$FLUTTER_DIR/bin/flutter" ]; then
flutter-version: ${{ env.flutter_version }} echo "Downloading Flutter ${{ env.flutter_version }}..."
cache: true curl -fsSL -o /tmp/flutter.tar.xz "https://storage.googleapis.com/flutter_infra_release/releases/${{ env.flutter_channel }}/linux/flutter_linux_${{ env.flutter_version }}-${{ env.flutter_channel }}.tar.xz"
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache echo "Extracting Flutter..."
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path mkdir -p "${{ env.my_local_cache_dir }}"
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:" # optional, change this to force refresh cache of dart pub get dependencies tar xf /tmp/flutter.tar.xz -C "${{ env.my_local_cache_dir }}"
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:" # optional, change this to specify the cache path rm /tmp/flutter.tar.xz
else
echo "Flutter already cached at $FLUTTER_DIR"
fi
echo "$FLUTTER_DIR/bin" >> $GITHUB_PATH
echo "$FLUTTER_DIR/bin/cache/dart-sdk/bin" >> $GITHUB_PATH
echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
# - name: Print flutter-action outputs # - name: Print flutter-action outputs
# shell: bash # shell: bash
@@ -126,15 +147,18 @@ jobs:
- name: Fix dir permissions - name: Fix dir permissions
run: | run: |
git config --global --add safe.directory /opt/hostedtoolcache/flutter/${{ env.flutter_channel }}-${{ env.flutter_version }}-${{ steps.flutter-action.outputs.ARCHITECTURE }} ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then ARCH="x64"; fi
git config --global --add safe.directory "${{ env.my_local_cache_dir }}flutter"
flutter config --android-sdk="$ANDROID_HOME" flutter config --android-sdk="$ANDROID_HOME"
flutter doctor -v flutter doctor -v
- name: Get dependencies - name: Get dependencies
run: | run: |
flutter clean flutter clean
flutter pub get flutter pub cache repair
dart run build_runner build --delete-conflicting-outputs flutter pub get
dart run build_runner build --delete-conflicting-outputs
- name: Native Analyze - name: Native Analyze
run: flutter analyze run: flutter analyze
@@ -142,19 +166,66 @@ jobs:
- name: Test - name: Test
run: flutter test --coverage run: flutter test --coverage
- name: Test coverage # - name: Test coverage
run: | # run: |
awk -F: ' # echo "📊 Analyzing test coverage..."
/^LF:/ { total += $2 } # echo ""
/^LH:/ { covered += $2 } # awk -F: '
END { # /^SF:/ {
if (total == 0) { # file = $0
print "Coverage: 0%" # sub(/^SF:/, "", file)
} else { # sub(/^[[:space:]]+/, "", file)
printf "Coverage: %.2f%%\n", (covered / total) * 100 # is_generated = 0
} # if (file ~ /\.(g|freezed|gr|mocks)\.dart$/) {
} # is_generated = 1
' coverage/lcov.info # 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 < 70) {
# printf "❌ FAIL: Coverage is below 70%%\n"
# exit 1
# } else {
# printf "💚 Excellent: Coverage is above 70%%\n"
# }
# }
# }
# ' coverage/lcov.info
- name: Build apk - name: Build apk
run: | run: |
@@ -169,10 +240,10 @@ jobs:
APK_NAME="eatvio_app_${TIMESTAMP}.apk" APK_NAME="eatvio_app_${TIMESTAMP}.apk"
mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME mv build/app/outputs/flutter-apk/app-release.apk $APK_NAME
echo "APK build finished $APK_NAME" echo "APK build finished $APK_NAME"
echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV echo "APK_NAME=$APK_NAME" >> $GITHUB_ENV
- name: Upload APK artifact - name: Upload APK artifact
uses: actions/upload-artifact@v3 uses: https://github.com/actions/upload-artifact@v3
with: with:
name: ${{ env.APK_NAME }} name: ${{ env.APK_NAME }}
path: ${{ env.APK_NAME }} path: ${{ env.APK_NAME }}

7
.gitignore vendored
View File

@@ -19,4 +19,9 @@ ios/XCBuildData/
env/.env env/.env
.env .env
android/key.properties android/key.properties
app/upload-keystore.jks app/upload-keystore.jks
coverage/
backend/
.fvm/
.fvmrc
/eatvio

View File

@@ -1,7 +1,7 @@
# Current Context # Current Context
## Project Status ## Project Status
Memory Bank initialized on 2025-10-16. This is a mature Flutter application (v1.3.0+79) currently in production. Memory Bank initialized on 2025-10-16. This is a mature Flutter application (v1.3.24+106) currently in production.
## Recent Activity ## Recent Activity
- Memory Bank initialization completed - Memory Bank initialization completed
@@ -9,10 +9,19 @@ Memory Bank initialized on 2025-10-16. This is a mature Flutter application (v1.
- Architecture and technology stack catalogued - Architecture and technology stack catalogued
## Current Focus ## Current Focus
Ready for development tasks. No active work in progress. Виджет видеоплеера ленты (`lib/features/feed/presentation/widgets/feed_video_player_widget.dart`)
мигрирован с `webview_flutter` v4 на `flutter_inappwebview` 6.1.5. Цель — фикс
fullscreen-кнопки внутри embed-плеера (Rutube/YouTube): `webview_flutter` не реализует
Android `WebChromeClient.onShowCustomView/onHideCustomView`, а `flutter_inappwebview`
делает это нативно (`InAppWebViewChromeClient`). Паритет UX сохранён (превью → play →
скрытая загрузка → fade-in по Rutube `player:canPlay`, плюс страховочный таймер 4с
для провайдеров без этого протокола). Добавлена ориентация landscape на fullscreen.
Остальные 4 экрана на `webview_flutter` не трогались; обе зависимости в `pubspec.yaml`.
## Known Issues ## Known Issues
None documented at initialization. - 2 теста в `test/features/feed/` (`post_feed_pagination_model_test`,
`post_feed_mapper_test`) падают на ветке `personalized_feed` — пре-существующие,
связаны с fixtures/маппингом, не с видеоплеером.
## Next Steps ## Next Steps
Awaiting user direction for: Awaiting user direction for:

View File

@@ -7,4 +7,7 @@ run-analyse:
run-checks: run-analyse run-tests run-checks: run-analyse run-tests
build-generated-files: 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
View 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()

View 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
View 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

View File

@@ -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"
}
} ]
}

View File

@@ -4,6 +4,7 @@ plugins {
id 'com.google.gms.google-services' id 'com.google.gms.google-services'
// END: FlutterFire Configuration // END: FlutterFire Configuration
id "kotlin-android" id "kotlin-android"
id "kotlin-parcelize"
id "dev.flutter.flutter-gradle-plugin" id "dev.flutter.flutter-gradle-plugin"
id 'com.huawei.agconnect' id 'com.huawei.agconnect'
} }
@@ -68,10 +69,12 @@ android {
} }
dependencies { dependencies {
implementation 'com.huawei.agconnect:agconnect-core:1.9.1.301' implementation platform('com.google.firebase:firebase-bom:34.4.0')
implementation 'com.huawei.hms:push:6.12.0.300' implementation 'com.google.firebase:firebase-messaging'
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 // Huawei Analytics dependency to satisfy com.huawei.hianalytics.* references during R8
implementation 'com.huawei.hms:hianalytics:6.12.0.300' implementation 'com.huawei.hms:hianalytics:6.12.0.301'
// BouncyCastle provider required by Huawei secure utils used transitively by HMS // BouncyCastle provider required by Huawei secure utils used transitively by HMS
implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1' implementation 'org.bouncycastle:bcprov-jdk15to18:1.78.1'
} }

View File

@@ -4,6 +4,12 @@
-keep class org.bouncycastle.** { *; } -keep class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.** -dontwarn org.bouncycastle.**
-keep class com.hiennv.flutter_callkit_incoming.** { *; }
-dontwarn com.hiennv.flutter_callkit_incoming.**
-keep class com.pravera.flutter_foreground_task.** { *; }
-dontwarn com.pravera.flutter_foreground_task.**
-keep class io.flutter.** { *; } -keep class io.flutter.** { *; }
-dontwarn io.flutter.** -dontwarn io.flutter.**

View File

@@ -1,7 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
@@ -11,6 +17,16 @@
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<service
android:name=".EatvioFcmMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service
android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService"
tools:node="remove" />
<service <service
android:name=".HmsPushService" android:name=".HmsPushService"
android:exported="false"> android:exported="false">
@@ -21,11 +37,17 @@
<meta-data <meta-data
android:name="push_kit_auto_init_enabled" android:name="push_kit_auto_init_enabled"
android:value="true" /> android:value="true" />
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="appid=113238569" />
<meta-data
android:name="com.huawei.hms.client.cpid"
android:value="cpid=20007000036693451" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity="" android:supportsPictureInPicture="true"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
@@ -49,32 +71,19 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<!-- Main domains -->
<data android:host="eatvio.ru" /> <data android:host="eatvio.ru" />
<data android:host="www.eatvio.ru" /> <data android:host="www.eatvio.ru" />
<data android:host="eatvio.com" />
<data android:host="www.eatvio.com" />
<!-- Development/Subdomains -->
<data android:host="*.eatvio.ru" />
<data android:host="*.eatvio.com" />
<data android:pathPattern="/ru/recipe/.*" /> <!-- Simplify patterns to allow navigation within the app for all matching hosts -->
<data android:pathPattern="/en/recipe/.*" /> <data android:pathPattern="/.*" />
<data android:pathPattern="/ru/blog_post/.*" />
<data android:pathPattern="/en/blog_post/.*" />
<data android:pathPattern="/ru/diet/.*" />
<data android:pathPattern="/en/diet/.*" />
<data android:pathPattern="/ru/plan/.*" />
<data android:pathPattern="/en/plan/.*" />
<data android:pathPattern="/ru/chat/.*" />
<data android:pathPattern="/en/chat/.*" />
<data android:pathPattern="/ru/chat" />
<data android:pathPattern="/en/chat" />
<data android:pathPattern="/ru/profile/.*" />
<data android:pathPattern="/en/profile/.*" />
<data android:pathPattern="/ru/diary/.*" />
<data android:pathPattern="/en/diary/.*" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
@@ -82,6 +91,68 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- Tap on notification body opens CallkitIncomingActivity via PendingIntent.
Replace it with a bridge that routes to the same accept flow as Answer. -->
<activity
android:name="com.hiennv.flutter_callkit_incoming.CallkitIncomingActivity"
tools:node="remove" />
<activity
android:name=".EatvioCallkitIncomingBridgeActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:noHistory="true"
android:turnScreenOn="true"
android:launchMode="singleTop"
android:configChanges="orientation" />
<activity-alias
android:name="com.hiennv.flutter_callkit_incoming.CallkitIncomingActivity"
android:targetActivity=".EatvioCallkitIncomingBridgeActivity"
android:exported="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:turnScreenOn="true"
android:launchMode="singleTop"
android:configChanges="orientation"
android:permission="${applicationId}.PERMISSION_CALL">
<intent-filter>
<action android:name="${applicationId}.com.hiennv.flutter_callkit_incoming.ACTION_CALL_INCOMING" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>
<activity
android:name="com.hiennv.flutter_callkit_incoming.TransparentActivity"
tools:node="remove" />
<activity
android:name=".EatvioCallkitTransparentBridgeActivity"
android:exported="false"
android:theme="@style/TranslucentTheme"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:launchMode="singleTop" />
<activity-alias
android:name="com.hiennv.flutter_callkit_incoming.TransparentActivity"
android:targetActivity=".EatvioCallkitTransparentBridgeActivity"
android:exported="false"
android:theme="@style/TranslucentTheme"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:launchMode="singleTop" />
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="microphone"
android:exported="false"
android:stopWithTask="true" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and
@@ -93,5 +164,168 @@
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
<!-- SBP bank packages for СБП payment detection -->
<package android:name="ru.sberbankmobile" />
<package android:name="com.idamob.tinkoff.android" />
<package android:name="ru.vtb24.mobilebanking.android" />
<package android:name="ru.alfabank.mobile.android" />
<package android:name="ru.raiffeisennews" />
<package android:name="com.openbank" />
<package android:name="ru.gazprombank.android.mobilebank.app" />
<package android:name="com.logo.mbanking" />
<package android:name="ru.sovcomcard.halva.v1" />
<package android:name="ru.rosbank.android" />
<package android:name="ru.rshb.dbo" />
<package android:name="ru.mkb.mobile" />
<package android:name="ru.ucb.android" />
<package android:name="ru.unicredit.android" />
<package android:name="ru.skbbank.ib" />
<package android:name="ru.gebank.ib" />
<package android:name="ru.avangard.sbpent" />
<package android:name="ru.elplat.elplat2" />
<package android:name="com.bifit.rncbbeta" />
<package android:name="ru.ftc.faktura.expobank" />
<package android:name="com.bssys.vbrrretail" />
<package android:name="ru.wildberries.razz" />
<package android:name="ru.artsofte.russiafl" />
<package android:name="ru.ftc.faktura.multibank" />
<package android:name="ru.bank_hlynov.xbank" />
<package android:name="ru.ftc.faktura.vesta" />
<package android:name="com.isimplelab.ibank.solidarnost" />
<package android:name="com.bssys.roscapretail" />
<package android:name="ru.ftc.faktura.kbhmb" />
<package android:name="ru.mts.money" />
<package android:name="ru.ftc.faktura.pskb" />
<package android:name="ru.ftc.faktura.nskbl" />
<package android:name="ru.akbars.mobile" />
<package android:name="ru.ftc.faktura.rostfinance" />
<package android:name="ru.ilb.bbmobile" />
<package android:name="ru.mcbankrus.MCBankRus" />
<package android:name="ru.albank.online.aebit" />
<package android:name="com.bifit.mobile.citizen.icbru" />
<package android:name="ru.ftc.faktura.orbank" />
<package android:name="ru.ftc.faktura.agropromkredit" />
<package android:name="com.energobank.digital" />
<package android:name="com.bifit.mobile.citizen.kbnk" />
<package android:name="ru.ftc.faktura.sdm" />
<package android:name="com.bifit.mobile.citizen.mbb" />
<package android:name="ru.ftc.faktura.absolutbank" />
<package android:name="ru.ftc.faktura.akcept" />
<package android:name="ru.faktura.etbank.person" />
<package android:name="ru.ftc.faktura.razvitiestolica" />
<package android:name="ru.ftc.faktura.resokreditbank" />
<package android:name="ru.yoo.money" />
<package android:name="ru.bspb" />
<package android:name="com.isimplelab.ibank.kubankredit" />
<package android:name="com.tochka.bank" />
<package android:name="com.bssys.novikomretail" />
<package android:name="ru.rosdorbank.physical" />
<package android:name="ru.ftc.faktura.primsoc" />
<package android:name="ru.ftc.faktura.banksaratov" />
<package android:name="ru.ftc.faktura.tavrich" />
<package android:name="com.bifit.mobile.citizen.thbank" />
<package android:name="ru.ftc.faktura.kremlevskiy" />
<package android:name="ru.ftc.faktura.tkbbank" />
<package android:name="ru.chelyabinvestbank.investpay" />
<package android:name="ru.ftc.faktura.derzhava" />
<package android:name="ru.nbd.android" />
<package android:name="ru.sngb.dbo.client.android" />
<package android:name="ru.ftc.faktura.united" />
<package android:name="ru.ftc.faktura.venetsbank" />
<package android:name="ru.letobank.Prometheus" />
<package android:name="ru.simpls.brs2.mobbank" />
<package android:name="com.bifit.dvbank" />
<package android:name="ru.ftc.faktura.intesabank" />
<package android:name="com.mmonline.mobile" />
<package android:name="cb.ibank.vuz" />
<package android:name="ru.uralprombank.mobilebanknew.googleplay" />
<package android:name="ru.ftc.faktura.nsbank" />
<package android:name="ru.emb.android" />
<package android:name="ru.ftc.faktura.maritimebank" />
<package android:name="com.credituralbank.CUBmobilenew" />
<package android:name="com.isimplelab.isimpleceo.uglemet" />
<package android:name="com.bssys.stroylesretail" />
<package android:name="ru.ftc.faktura.iturup" />
<package android:name="ru.ftc.faktura.finbank" />
<package android:name="ru.faktura.gaztransbank.person" />
<package android:name="com.isimplelab.ibank.chelind" />
<package android:name="ru.ftc.faktura.nokss" />
<package android:name="ru.ftc.faktura.vlbb" />
<package android:name="ru.ftc.faktura.kbb" />
<package android:name="ru.faktura.avtograd.person" />
<package android:name="ru.ftc.faktura.forshtadt" />
<package android:name="ru.ftc.faktura.solidbank" />
<package android:name="ru.ftc.faktura.alexbank" />
<package android:name="ru.ftc.faktura.akibank" />
<package android:name="ru.ftc.faktura.nipbank" />
<package android:name="ru.ftc.faktura.alefbank" />
<package android:name="com.bifit.vfbank" />
<package android:name="com.bssys.uralfdretail" />
<package android:name="com.snb.online" />
<package android:name="com.bifit.mobile.citizen.okbank" />
<package android:name="com.tatsotsbank.dbomobile" />
<package android:name="ru.vtkbank.android" />
<package android:name="ru.faktura.jtbank.person" />
<package android:name="ru.ftc.faktura.ns" />
<package android:name="ru.ftc.faktura.zemskybank" />
<package android:name="com.bssys.aversretail" />
<package android:name="ru.rusnarbank.correqts.retail" />
<package android:name="ru.ftc.faktura.baikalinvestbank" />
<package android:name="ru.sbi.android" />
<package android:name="com.metkombank.mobile" />
<package android:name="com.isimplelab.ionic.kazan.fl" />
<package android:name="com.intervale.sbpas" />
<package android:name="com.bifit.mobile.citizen.zarech" />
<package android:name="ru.centrinvest.mobilebanking2018" />
<package android:name="com.mifors.izhcombank" />
<package android:name="com.bifit.pmobile.isbank" />
<package android:name="ru.zenit.android" />
<package android:name="ru.briginvest.sense" />
<package android:name="com.bifit.mobile.citizen.cfb" />
<package android:name="com.isimplelab.ionic.gorbank.prod" />
<package android:name="ru.ftc.faktura.moscombank" />
<package android:name="com.timerbank.retail" />
<package android:name="com.mob.android" />
<package android:name="com.citibank.mobile.ru" />
<package android:name="com.bifit.atbbank" />
<package android:name="ru.mw" />
<package android:name="ru.bankuralsib.mb.android" />
<package android:name="com.banksoyuz.artsofte" />
<package android:name="ru.rubank.ubsmobile" />
<package android:name="cb.ibank" />
<package android:name="ru.ipb.ubsmobile" />
<package android:name="com.openwaygroup.ic.panda.poidem" />
<package android:name="com.bifit.mobile.citizen.tenderbank" />
<package android:name="ru.otpbank.mobile" />
<package android:name="ru.krk.ubsmobile" />
<package android:name="ru.homecredit.mycredit" />
<package android:name="cz.bsc.rc" />
<package android:name="com.bifit.mobile.citizen.belsocbank" />
<package android:name="com.hicebank.android" />
<package android:name="com.bpc.crossplatform_trading.bpc_trading" />
<package android:name="com.bifit.nrb" />
<package android:name="com.bifit.mobile.citizen.moskb" />
<package android:name="ru.isfront.android.kt" />
<package android:name="ru.ftc.faktura.novobank" />
<package android:name="com.smp.android" />
<package android:name="com.bifit.mobile.citizen.nkbank" />
<package android:name="com.isimplelab.ibank.toyota" />
<package android:name="com.bifit.mobile.private.bbr" />
<package android:name="ru.ftc.faktura.lanta" />
<package android:name="com.yandex.bank" />
<package android:name="ru.ftc.faktura.bgfbank" />
<package android:name="com.idamobile.android.LockoBank" />
<package android:name="com.bifit.mobile.citizen.MCBank" />
<package android:name="com.cetelem.cetelem_android" />
<package android:name="com.bssys.gutaretail" />
<package android:name="ru.ftc.faktura.siab" />
<package android:name="com.isimplelab.ionic.round.prod" />
<package android:name="com.priobank.prio" />
<package android:name="com.inbank.mobilebank" />
<package android:name="com.isimplelab.isimplemobile.payjet" />
<package android:name="ru.ftc.faktura.agroros" />
<package android:name="com.bifit.mobile.citizen.invb" />
<package android:name="com.compassplus.mobicash.customer" />
<package android:name="ru.finambank.app" />
</queries> </queries>
</manifest> </manifest>

View File

@@ -0,0 +1,53 @@
package com.eatvioapp.app
import android.content.Intent
import android.os.Bundle
import com.hiennv.flutter_callkit_incoming.CallkitConstants
import com.hiennv.flutter_callkit_incoming.FlutterCallkitIncomingPlugin
object CallKitAcceptIntentMapper {
fun payloadFromBundle(data: Bundle, action: String): Map<String, Any?>? {
if (!action.contains(CallkitConstants.ACTION_CALL_ACCEPT) &&
!action.contains(CallkitConstants.ACTION_CALL_CALLBACK)
) {
return null
}
return hashMapOf(
"action" to action,
"id" to data.getString(CallkitConstants.EXTRA_CALLKIT_ID, ""),
"handle" to data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, ""),
"nameCaller" to data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, ""),
"avatar" to data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, ""),
"extra" to data.getSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA),
)
}
fun payloadFromIntent(intent: Intent?): Map<String, Any?>? {
if (intent == null) return null
val data = intent.getBundleExtra(FlutterCallkitIncomingPlugin.EXTRA_CALLKIT_CALL_DATA)
?: return null
val action = intent.action ?: return null
if (!action.contains(CallkitConstants.ACTION_CALL_ACCEPT) &&
!action.contains(CallkitConstants.ACTION_CALL_CALLBACK)
) {
return null
}
return hashMapOf(
"action" to action,
"id" to data.getString(CallkitConstants.EXTRA_CALLKIT_ID, ""),
"handle" to data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, ""),
"nameCaller" to data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, ""),
"avatar" to data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, ""),
"extra" to data.getSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA),
)
}
fun clearCallKitLaunchIntent(intent: Intent?) {
if (intent == null) return
intent.removeExtra(FlutterCallkitIncomingPlugin.EXTRA_CALLKIT_CALL_DATA)
intent.action = null
}
}

View File

@@ -0,0 +1,38 @@
package com.eatvioapp.app
import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.hiennv.flutter_callkit_incoming.AppUtils
import com.hiennv.flutter_callkit_incoming.CallkitIncomingBroadcastReceiver
object CallKitIntentBridge {
fun dispatchCallKitAction(context: Context, action: String, data: Bundle?) {
if (data != null) {
CallKitAcceptIntentMapper.payloadFromBundle(data, action)?.let { payload ->
PendingCallKitAcceptBridge.store(payload)
}
}
val broadcastIntent = CallkitIncomingBroadcastReceiver.getIntent(context, action, data)
broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
context.sendBroadcast(broadcastIntent)
val shouldLaunchMainActivity =
action == com.hiennv.flutter_callkit_incoming.CallkitConstants.ACTION_CALL_ACCEPT ||
action == com.hiennv.flutter_callkit_incoming.CallkitConstants.ACTION_CALL_CALLBACK ||
!MainActivity.isInForeground
if (!shouldLaunchMainActivity) {
return
}
val activityIntent = AppUtils.getAppIntent(context, action, data) ?: return
activityIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT,
)
context.startActivity(activityIntent)
}
}

View File

@@ -0,0 +1,29 @@
package com.eatvioapp.app
import android.app.Activity
import android.os.Bundle
import com.hiennv.flutter_callkit_incoming.CallkitConstants
/// Redirects notification body / full-screen intent taps to the same accept path as
/// the Answer button, instead of showing the native Callkit incoming UI.
class EatvioCallkitIncomingBridgeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA)
if (data == null) {
finish()
return
}
CallKitIntentBridge.dispatchCallKitAction(
this,
CallkitConstants.ACTION_CALL_ACCEPT,
data,
)
finish()
@Suppress("DEPRECATION")
overridePendingTransition(0, 0)
}
}

View File

@@ -0,0 +1,25 @@
package com.eatvioapp.app
import android.app.Activity
import android.os.Bundle
/// Replaces plugin TransparentActivity: forwards CallKit actions without relaunching
/// MainActivity when the Flutter app is already in the foreground.
class EatvioCallkitTransparentBridgeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val action = intent.action
if (action.isNullOrBlank()) {
finish()
return
}
val data = intent.getBundleExtra("data")
CallKitIntentBridge.dispatchCallKitAction(this, action, data)
finish()
@Suppress("DEPRECATION")
overridePendingTransition(0, 0)
}
}

View File

@@ -0,0 +1,38 @@
package com.eatvioapp.app
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.flutter.plugins.firebase.messaging.FlutterFirebaseTokenLiveData
class EatvioFcmMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
FlutterFirebaseTokenLiveData.getInstance().postToken(token)
Log.i(TAG, "onNewToken")
}
override fun onMessageReceived(message: RemoteMessage) {
Log.i(TAG, "onMessageReceived: ${message.data}")
if (VideoCallPushBridge.isEndedVideoCall(message.data)) {
VideoCallPushBridge.dismissAllIncomingCalls(this)
return
}
if (!VideoCallPushBridge.isIncomingVideoCall(message.data)) {
return
}
if (MainActivity.isInForeground) {
Log.i(TAG, "App in foreground, deferring incoming call to Flutter handler")
return
}
VideoCallPushBridge.showIncomingCall(this, message.data)
}
companion object {
private const val TAG = "EatvioFCM"
}
}

View File

@@ -1,6 +1,10 @@
package com.eatvioapp.app package com.eatvioapp.app
import android.app.PendingIntent
import android.content.Intent
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage import com.huawei.hms.push.RemoteMessage
@@ -13,7 +17,50 @@ class HmsPushService : HmsMessageService() {
override fun onMessageReceived(message: RemoteMessage?) { override fun onMessageReceived(message: RemoteMessage?) {
super.onMessageReceived(message) super.onMessageReceived(message)
Log.i("HMS", "onMessageReceived: ${message?.data}") Log.i("HMS", "onMessageReceived: ${message?.data}")
if (message == null) return
if (VideoCallPushBridge.isEndedVideoCall(message.dataOfMap)) {
VideoCallPushBridge.dismissAllIncomingCalls(this)
return
}
if (VideoCallPushBridge.isIncomingVideoCall(message.dataOfMap)) {
VideoCallPushBridge.showIncomingCall(this, message.dataOfMap)
return
}
val actionUrl = message.dataOfMap["action_url"]
showNotification(message, actionUrl)
}
private fun showNotification(message: RemoteMessage, actionUrl: String?) {
val context = applicationContext
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
if (!actionUrl.isNullOrBlank()) {
putExtra("action_url", actionUrl)
}
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val channelId = "hms_push_channel"
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(message.notification?.title ?: context.getString(R.string.app_name))
.setContentText(message.notification?.body ?: "")
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
NotificationManagerCompat.from(context).notify(message.messageId.hashCode(), notification)
} }
} }

View File

@@ -1,11 +1,16 @@
package com.eatvioapp.app package com.eatvioapp.app
import android.app.PictureInPictureParams
import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Rational
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import com.hiennv.flutter_callkit_incoming.FlutterCallkitIncomingPlugin
import com.huawei.agconnect.config.AGConnectServicesConfig import com.huawei.agconnect.config.AGConnectServicesConfig
import com.huawei.hms.aaid.HmsInstanceId import com.huawei.hms.aaid.HmsInstanceId
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -13,13 +18,69 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
companion object {
@JvmStatic
var isInForeground: Boolean = false
private set
}
private val channelName = "com.eatvioapp/hms_push" private val channelName = "com.eatvioapp/hms_push"
private val videoCallPipChannelName = "com.eatvioapp/video_call_pip"
private val videoCallPushChannelName = "com.eatvioapp/video_call_push"
private var methodChannel: MethodChannel? = null
private var videoCallPipChannel: MethodChannel? = null
private var videoCallPushChannel: MethodChannel? = null
private var videoCallPipEnabled: Boolean = false
override fun onResume() {
super.onResume()
isInForeground = true
captureCallKitLaunchIntent(intent)
if (PendingCallKitAcceptBridge.peek() != null) {
scheduleDeliverPendingCallKitAccept()
}
}
override fun onPause() {
isInForeground = false
super.onPause()
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName) methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
.setMethodCallHandler { call, result -> videoCallPipChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, videoCallPipChannelName)
videoCallPushChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, videoCallPushChannelName)
videoCallPushChannel?.setMethodCallHandler { call, result ->
when (call.method) {
"dismissIncomingCalls" -> {
try {
VideoCallPushBridge.dismissAllIncomingCalls(applicationContext)
result.success(null)
} catch (e: Exception) {
result.error("DISMISS_INCOMING_CALLS_FAILED", e.message, null)
}
}
"drainPendingCallKitAccept" -> {
result.success(PendingCallKitAcceptBridge.drain())
}
"peekPendingCallKitAccept" -> {
result.success(PendingCallKitAcceptBridge.peekPending())
}
else -> result.notImplemented()
}
}
videoCallPipChannel?.setMethodCallHandler { call, result ->
when (call.method) {
"setPipEnabled" -> {
videoCallPipEnabled = call.arguments as? Boolean ?: false
result.success(null)
}
else -> result.notImplemented()
}
}
methodChannel?.setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"getHuaweiToken" -> { "getHuaweiToken" -> {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
@@ -47,17 +108,21 @@ class MainActivity: FlutterActivity() {
} }
if (appId.isNullOrBlank()) { if (appId.isNullOrBlank()) {
android.util.Log.e("HMS", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.")
runOnUiThread { runOnUiThread {
result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null) result.error("HMS_TOKEN_ERROR", "Missing Huawei appId. Ensure agconnect-services.json or manifest meta-data is configured.", null)
} }
return@launch return@launch
} }
android.util.Log.i("HMS", "Getting token for appId: $appId")
val token = HmsInstanceId.getInstance(applicationContext) val token = HmsInstanceId.getInstance(applicationContext)
.getToken(appId, "HCM") .getToken(appId, "HCM")
android.util.Log.i("HMS", "Token retrieved successfully")
runOnUiThread { runOnUiThread {
result.success(token) result.success(token)
} }
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("HMS", "Error getting HMS token: ${e.message}", e)
runOnUiThread { runOnUiThread {
result.error("HMS_TOKEN_ERROR", e.message, null) result.error("HMS_TOKEN_ERROR", e.message, null)
} }
@@ -67,5 +132,98 @@ class MainActivity: FlutterActivity() {
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
// Обрабатываем возможный action_url, с которым была открыта Activity (например, из HMS уведомления).
handleIntentForActionUrl(intent)
captureCallKitLaunchIntent(intent)
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
enterVideoCallPictureInPictureIfNeeded()
videoCallPipChannel?.invokeMethod("onUserLeaveHint", null)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
captureCallKitLaunchIntent(intent)
// Обработка нового интента, когда Activity уже запущена (тап по уведомлению).
handleIntentForActionUrl(intent)
}
private fun captureCallKitLaunchIntent(intent: Intent?) {
val payload = CallKitAcceptIntentMapper.payloadFromIntent(intent) ?: return
android.util.Log.i(
"VideoCallPush",
"captureCallKitLaunchIntent: action=${payload["action"]} id=${payload["id"]}",
)
PendingCallKitAcceptBridge.store(payload)
CallKitAcceptIntentMapper.clearCallKitLaunchIntent(intent)
scheduleDeliverPendingCallKitAccept()
}
private fun scheduleDeliverPendingCallKitAccept() {
PendingCallKitAcceptBridge.scheduleDeliverToFlutter(
deliver = { deliverPendingCallKitAcceptToFlutter() },
)
}
private fun deliverPendingCallKitAcceptToFlutter() {
val payload = PendingCallKitAcceptBridge.peek() ?: return
val channel = videoCallPushChannel
if (channel == null) {
android.util.Log.w(
"VideoCallPush",
"deliverPendingCallKitAcceptToFlutter: channel not ready",
)
return
}
android.util.Log.i(
"VideoCallPush",
"deliverPendingCallKitAcceptToFlutter: id=${payload["id"]}",
)
channel.invokeMethod("onCallKitLaunchIntent", payload)
}
private fun handleIntentForActionUrl(intent: Intent?) {
if (intent == null) return
if (intent.hasExtra(FlutterCallkitIncomingPlugin.EXTRA_CALLKIT_CALL_DATA)) {
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)
}
private fun enterVideoCallPictureInPictureIfNeeded() {
if (!videoCallPipEnabled) return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
if (isInPictureInPictureMode) return
try {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(2, 3))
.build()
enterPictureInPictureMode(params)
} catch (e: Exception) {
android.util.Log.w("VideoCallPiP", "Unable to enter PiP from onUserLeaveHint", e)
}
} }
} }

View File

@@ -0,0 +1,42 @@
package com.eatvioapp.app
import android.os.Handler
import android.os.Looper
import android.util.Log
/// Stores accepted call payloads until the Flutter engine is ready to drain them.
object PendingCallKitAcceptBridge {
private const val TAG = "PendingCallKitAccept"
@Volatile
private var pendingPayload: Map<String, Any?>? = null
private val mainHandler = Handler(Looper.getMainLooper())
fun store(payload: Map<String, Any?>) {
pendingPayload = payload
Log.i(TAG, "stored pending accept id=${payload["id"]}")
}
fun peek(): Map<String, Any?>? = pendingPayload
fun drain(): Map<String, Any?>? {
val payload = pendingPayload
pendingPayload = null
if (payload != null) {
Log.i(TAG, "drained pending accept id=${payload["id"]}")
}
return payload
}
fun peekPending(): Map<String, Any?>? = pendingPayload
fun scheduleDeliverToFlutter(
deliver: () -> Unit,
delaysMs: LongArray = longArrayOf(0L, 250L, 700L, 1500L, 2800L, 4200L, 6000L),
) {
for (delayMs in delaysMs) {
mainHandler.postDelayed({ deliver() }, delayMs)
}
}
}

View File

@@ -0,0 +1,318 @@
package com.eatvioapp.app
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.hiennv.flutter_callkit_incoming.CallkitConstants
import com.hiennv.flutter_callkit_incoming.CallkitNotificationManager
import com.hiennv.flutter_callkit_incoming.CallkitNotificationService
import com.hiennv.flutter_callkit_incoming.CallkitSoundPlayerManager
import com.hiennv.flutter_callkit_incoming.Data
import com.hiennv.flutter_callkit_incoming.FlutterCallkitIncomingPlugin
import com.hiennv.flutter_callkit_incoming.addCall
import com.hiennv.flutter_callkit_incoming.getDataActiveCalls
import com.hiennv.flutter_callkit_incoming.removeAllCalls
import java.util.HashMap
object VideoCallPushBridge {
private const val TAG = "VideoCallPushBridge"
private const val ANDROID_ACCENT_COLOR = "#2787F5"
private const val MISSED_CALL_CLEANUP_DELAY_MS = 250L
private var activeSoundManager: CallkitSoundPlayerManager? = null
private var activeNotificationManager: CallkitNotificationManager? = null
fun isIncomingVideoCall(data: Map<String, String>): Boolean {
val rawType = data["type"]
?: data["notification_type"]
?: data["event"]
?: data["action"]
?: data["push_type"]
?: data["call_type"]
if (rawType != null) {
val normalized = rawType.lowercase().replace("-", "_")
if (normalized.contains("video_call")) {
return !normalized.contains("end") && !normalized.contains("ended")
}
if (normalized == "incoming_call" ||
normalized == "video_call_incoming" ||
normalized == "incoming_video_call"
) {
return true
}
}
if (isTruthy(data["is_video_call"]) ||
isTruthy(data["incoming_call"]) ||
isTruthy(data["is_incoming_call"])
) {
return conversationUuid(data) != null
}
val actionUrl = data["action_url"]
if (!actionUrl.isNullOrBlank() &&
(actionUrl.contains("video-call") || actionUrl.contains("videoCall"))
) {
return true
}
return false
}
fun isEndedVideoCall(data: Map<String, String>): Boolean {
val rawType = data["type"]
?: data["notification_type"]
?: data["event"]
?: data["action"]
?: data["push_type"]
?: data["call_type"]
if (rawType != null) {
val normalized = rawType.lowercase().replace("-", "_")
if (normalized == "video_call_ended" || normalized == "video_call_end") {
return true
}
if (normalized.contains("video_call") &&
(normalized.contains("ended") || normalized.endsWith("_end"))
) {
return true
}
}
return false
}
fun showIncomingCall(context: Context, data: Map<String, String>) {
val appContext = context.applicationContext
val callData = buildCallDataBundle(data)
if (callData == null) {
Log.w(TAG, "showIncomingCall: missing conversation uuid, data=$data")
return
}
Log.i(TAG, "showIncomingCall: conversation=${callData.getString(CallkitConstants.EXTRA_CALLKIT_ID)}")
val soundManager = CallkitSoundPlayerManager(appContext)
val notificationManager = CallkitNotificationManager(appContext, soundManager)
activeSoundManager = soundManager
activeNotificationManager = notificationManager
notificationManager.createNotificationChanel(callData)
notificationManager.showIncomingNotification(callData)
try {
addCall(appContext, Data.fromBundle(callData))
} catch (error: Exception) {
Log.w(TAG, "showIncomingCall: failed to persist active call", error)
}
}
fun dismissAllIncomingCalls(context: Context) {
val appContext = context.applicationContext
val calls = getDataActiveCalls(appContext)
Log.i(TAG, "dismissAllIncomingCalls: active calls=${calls.size}")
stopIncomingRingtone(appContext)
val notificationManager = resolveNotificationManager(appContext)
val bundles = calls.map { it.toBundle() }
for (bundle in bundles) {
try {
dismissIncomingCallNotification(appContext, notificationManager, bundle)
} catch (error: Exception) {
Log.w(
TAG,
"dismissAllIncomingCalls: failed for call ${bundle.getString(CallkitConstants.EXTRA_CALLKIT_ID)}",
error,
)
}
}
try {
CallkitNotificationService.stopService(appContext)
} catch (error: Exception) {
Log.w(TAG, "dismissAllIncomingCalls: failed to stop callkit service", error)
}
try {
removeAllCalls(appContext)
} catch (error: Exception) {
Log.w(TAG, "dismissAllIncomingCalls: failed to remove persisted calls", error)
}
stopIncomingRingtone(appContext)
scheduleMissedCallCleanup(notificationManager, bundles)
activeSoundManager = null
activeNotificationManager = null
}
private fun resolveNotificationManager(context: Context): CallkitNotificationManager {
activeNotificationManager?.let { return it }
FlutterCallkitIncomingPlugin.getInstance()
?.getCallkitNotificationManager()
?.let { return it }
val soundManager = CallkitSoundPlayerManager(context)
return CallkitNotificationManager(context, soundManager)
}
private fun stopIncomingRingtone(context: Context) {
activeSoundManager?.stop()
FlutterCallkitIncomingPlugin.getInstance()
?.getCallkitSoundPlayerManager()
?.stop()
}
/**
* Replaces the incoming notification before cancel so Android does not fire the
* timeout delete intent (which would show a missed-call notification).
*/
private fun dismissIncomingCallNotification(
context: Context,
notificationManager: CallkitNotificationManager,
bundle: Bundle,
) {
val notificationId = incomingNotificationId(bundle)
val silentNotification = NotificationCompat.Builder(
context,
CallkitNotificationManager.NOTIFICATION_CHANNEL_ID_INCOMING,
)
.setSmallIcon(context.applicationInfo.icon)
.setContentTitle("")
.setSilent(true)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_MIN)
.build()
val notificationManagerCompat = NotificationManagerCompat.from(context)
notificationManagerCompat.notify(notificationId, silentNotification)
notificationManagerCompat.cancel(notificationId)
bundle.putBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW, false)
notificationManager.clearIncomingNotification(bundle, isAccepted = true)
notificationManager.clearMissCallNotification(bundle)
}
private fun scheduleMissedCallCleanup(
notificationManager: CallkitNotificationManager,
bundles: List<Bundle>,
) {
if (bundles.isEmpty()) return
Handler(Looper.getMainLooper()).postDelayed({
for (bundle in bundles) {
try {
notificationManager.clearMissCallNotification(bundle)
} catch (error: Exception) {
Log.w(TAG, "dismissAllIncomingCalls: delayed missed-call cleanup failed", error)
}
}
}, MISSED_CALL_CLEANUP_DELAY_MS)
}
private fun incomingNotificationId(bundle: Bundle): Int {
return bundle.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode()
}
private fun buildCallDataBundle(data: Map<String, String>): Bundle? {
val conversationUuid = conversationUuid(data) ?: return null
val callerName = data["counterpartName"]
?: data["counterpart_name"]
?: data["caller_name"]
?: data["callerName"]
?: data["nameCaller"]
?: data["caller"]
?: "Итвио"
val avatarUrl = data["counterpartAvatarUrl"]
?: data["counterpart_avatar_url"]
?: data["avatar"]
?: data["avatarUrl"]
?: data["avatar_url"]
?: data["caller_avatar"]
?: ""
val localParticipantName = data["localParticipantName"]
?: data["local_participant_name"]
val counterpartIsConsultant = isTruthy(
data["counterpartIsConsultant"]
?: data["counterpart_is_consultant"]
?: data["is_consultant"]
?: data["isConsultant"]
)
val extra = HashMap<String, Any?>()
extra["conversationUuid"] = conversationUuid
extra["counterpartName"] = callerName
if (avatarUrl.isNotBlank()) {
extra["counterpartAvatarUrl"] = avatarUrl
}
if (!localParticipantName.isNullOrBlank()) {
extra["localParticipantName"] = localParticipantName
}
if (counterpartIsConsultant) {
extra["counterpartIsConsultant"] = true
}
return Bundle().apply {
putString(CallkitConstants.EXTRA_CALLKIT_ID, conversationUuid)
putString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, callerName)
putString(CallkitConstants.EXTRA_CALLKIT_APP_NAME, "Итвио")
putString(CallkitConstants.EXTRA_CALLKIT_HANDLE, conversationUuid)
putString(CallkitConstants.EXTRA_CALLKIT_AVATAR, avatarUrl)
putInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0)
putLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 45000L)
putString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "Answer")
putString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "Decline")
putSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA, extra)
putSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS, HashMap<String, Any?>())
putBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, true)
putBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION, false)
putBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_LOGO, false)
putBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false)
putString(CallkitConstants.EXTRA_CALLKIT_RINGTONE_PATH, "system_ringtone_default")
putString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR, ANDROID_ACCENT_COLOR)
putString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, ANDROID_ACCENT_COLOR)
putString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, "#ffffff")
putBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_FULL_LOCKED_SCREEN, false)
putBoolean(CallkitConstants.EXTRA_CALLKIT_IS_FULL_SCREEN, false)
putBoolean(CallkitConstants.EXTRA_CALLKIT_CALLING_SHOW, false)
}
}
private fun conversationUuid(data: Map<String, String>): String? {
val fromData = data["conversationUuid"]
?: data["conversation_uuid"]
?: data["conversation_id"]
?: data["chat_uuid"]
?: data["uuid"]
if (!fromData.isNullOrBlank()) {
return fromData
}
val actionUrl = data["action_url"] ?: return null
val uuidFromQuery = Regex("""[?&](?:uuid|conversationUuid|conversation_uuid)=([^&]+)""")
.find(actionUrl)
?.groupValues
?.getOrNull(1)
?.trim()
if (!uuidFromQuery.isNullOrEmpty()) {
return uuidFromQuery
}
val pathMatch = Regex("""/video-call/([^/?#]+)""").find(actionUrl)
return pathMatch?.groupValues?.getOrNull(1)?.trim()?.takeIf { it.isNotEmpty() }
}
private fun isTruthy(value: String?): Boolean {
if (value.isNullOrBlank()) return false
return when (value.trim().lowercase()) {
"1", "true", "yes" -> true
else -> false
}
}
}

View File

@@ -650,7 +650,7 @@ code + .copy-button {
<script type="text/javascript"> <script type="text/javascript">
function configurationCacheProblems() { return ( function configurationCacheProblems() { return (
// begin-report-data // begin-report-data
{"diagnostics":[{"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/9.0.0/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('url = <value>') instead."}]]},{"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/9.0.0/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('url = <value>') instead."}]]}],"problemsReport":{"totalProblemCount":2,"buildName":"android","requestedTasks":":app:assembleProfile","documentationLink":"https://docs.gradle.org/9.0.0/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}} {"diagnostics":[{"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/9.0.0/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('url = <value>') instead."}]]}],"problemsReport":{"totalProblemCount":1,"requestedTasks":":app:compileDebugKotlin","documentationLink":"https://docs.gradle.org/9.0.0/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}}
// end-report-data // end-report-data
);} );}
</script> </script>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
<svg width="10" height="9" viewBox="0 0 10 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.53669 8.89177L6.8122 7.15382C6.52317 7.26749 6.22614 7.34957 5.92111 7.40005C5.61615 7.45053 5.3063 7.47578 4.99155 7.47578C3.95998 7.47578 3.01527 7.19683 2.15742 6.63893C1.29964 6.08096 0.619788 5.34562 0.117849 4.43293C0.0770142 4.36144 0.047164 4.28862 0.0282984 4.21447C0.00943278 4.14041 0 4.06506 0 3.98842C0 3.91178 0.0102087 3.8356 0.030626 3.75987C0.0510433 3.68423 0.0816693 3.61224 0.122504 3.5439C0.317857 3.21368 0.525501 2.89417 0.745437 2.58538C0.96529 2.2765 1.22096 2.00328 1.51243 1.76572L0.406101 0.633204C0.338315 0.564287 0.303606 0.478889 0.301972 0.377008C0.300421 0.275127 0.337662 0.185536 0.413696 0.108233C0.484667 0.0360776 0.570746 0 0.671934 0C0.773041 0 0.85908 0.0360776 0.93005 0.108233L9.05304 8.3668C9.12091 8.43579 9.15689 8.52119 9.16097 8.62299C9.16505 8.72487 9.12908 8.81446 9.05304 8.89177C8.98207 8.96392 8.89599 9 8.79481 9C8.6937 9 8.60766 8.96392 8.53669 8.89177ZM4.99155 6.01955C5.10025 6.01955 5.20405 6.01299 5.30295 5.99988C5.40194 5.98676 5.49855 5.95815 5.5928 5.91406L3.09751 3.37713C3.05725 3.47295 3.02989 3.57118 3.01544 3.67181C3.00098 3.77237 2.99375 3.8779 2.99375 3.98842C2.99375 4.5537 3.18772 5.03355 3.57565 5.42795C3.96358 5.82235 4.43554 6.01955 4.99155 6.01955ZM4.99155 0.501058C6.02622 0.501058 6.97529 0.781293 7.83878 1.34176C8.70227 1.90223 9.38344 2.64201 9.88227 3.56109C9.91992 3.62436 9.94896 3.69224 9.96937 3.76473C9.98979 3.83721 10 3.91178 10 3.98842C10 4.06506 9.99106 4.13962 9.97317 4.21211C9.95529 4.28459 9.92748 4.35247 9.88975 4.41574C9.74086 4.70378 9.57442 4.97521 9.39042 5.23004C9.20634 5.48487 9.00057 5.72471 8.77312 5.94956C8.67136 6.05302 8.55074 6.10059 8.41125 6.09229C8.27184 6.08399 8.15121 6.02811 8.04937 5.92465L7.0223 4.88517C6.97142 4.83344 6.93781 4.77369 6.92147 4.70594C6.90514 4.63827 6.90608 4.56769 6.92429 4.49421C6.94749 4.40868 6.96411 4.32565 6.97415 4.24511C6.98428 4.16465 6.98934 4.07909 6.98934 3.98842C6.98934 3.42313 6.79538 2.94329 6.40745 2.54888C6.01952 2.15448 5.54755 1.95728 4.99155 1.95728C4.90236 1.95728 4.81898 1.96322 4.74139 1.97509C4.66381 1.98688 4.58447 2.00461 4.50337 2.02827C4.43117 2.04994 4.35894 2.05248 4.28666 2.03587C4.21446 2.01926 4.15293 1.9851 4.10205 1.93337L3.64584 1.46967C3.50643 1.32785 3.46012 1.16274 3.50692 0.974343C3.55372 0.785942 3.67136 0.665878 3.85985 0.614149C4.04516 0.575788 4.23174 0.547349 4.41958 0.528832C4.60742 0.510316 4.79807 0.501058 4.99155 0.501058ZM5.94046 3.04758C6.03977 3.15876 6.1216 3.28347 6.18596 3.42172C6.25032 3.55997 6.29066 3.70449 6.30699 3.85527C6.31516 3.91531 6.29303 3.95699 6.2406 3.98032C6.18817 4.00357 6.139 3.9919 6.0931 3.94532L5.04153 2.88093C4.99563 2.83427 4.98542 2.78428 5.0109 2.73097C5.0363 2.67767 5.07852 2.65101 5.13757 2.65101C5.29593 2.66438 5.44199 2.70478 5.57577 2.7722C5.70962 2.83954 5.83119 2.93133 5.94046 3.04758Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3438 5.34375C14.9405 5.34375 15.5128 5.5808 15.9347 6.00276C16.3567 6.42472 16.5938 6.99701 16.5938 7.59375M14.3438 0.84375C16.134 0.84375 17.8509 1.55491 19.1167 2.82078C20.3826 4.08665 21.0938 5.80354 21.0938 7.59375M3.09375 1.96875H7.59375L9.84375 7.59375L7.03125 9.28125C8.23608 11.7242 10.2133 13.7014 12.6562 14.9062L14.3438 12.0938L19.9688 14.3438V18.8438C19.9688 19.4405 19.7317 20.0128 19.3097 20.4347C18.8878 20.8567 18.3155 21.0938 17.7188 21.0938C13.3304 20.8271 9.19142 18.9636 6.08268 15.8548C2.97394 12.7461 1.11043 8.60708 0.84375 4.21875C0.84375 3.62201 1.0808 3.04972 1.50276 2.62776C1.92472 2.2058 2.49701 1.96875 3.09375 1.96875Z" stroke="white" stroke-width="1.6875" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 629 B

View File

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

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

0
flutter Normal file
View File

View File

@@ -47,8 +47,6 @@ post_install do |installer|
## dart: PermissionGroup.photos ## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=1', 'PERMISSION_PHOTOS=1',
'PERMISSION_NOTIFICATIONS=1', 'PERMISSION_NOTIFICATIONS=1',
## dart: PermissionGroup.mediaLibrary
'PERMISSION_MEDIA_LIBRARY=1',
] ]
end end

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 83; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -397,7 +397,7 @@
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Eatvio; INFOPLIST_KEY_CFBundleDisplayName = Eatvio;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -539,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 83; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -548,7 +548,7 @@
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Eatvio; INFOPLIST_KEY_CFBundleDisplayName = Eatvio;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -578,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 83; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = VL68C36HYS; DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@@ -587,7 +587,7 @@
); );
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Eatvio; INFOPLIST_KEY_CFBundleDisplayName = Eatvio;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

View File

@@ -1,30 +1,517 @@
import AVFAudio
import CallKit
import Flutter import Flutter
import PushKit
import UIKit import UIKit
import UserNotifications
import YandexLoginSDK import YandexLoginSDK
import flutter_callkit_incoming
import flutter_foreground_task
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
private var voipRegistry: PKPushRegistry?
private var voipDiagnosticsChannel: FlutterMethodChannel?
private let voipDiagnosticsStorageKey = "EatvioVoipDiagnostics"
private let pendingAcceptedVoipCallKey = "EatvioPendingAcceptedVoipCall"
private let voipDiagnosticsChannelName = "com.eatvioapp/voip_diagnostics"
private let appUrlScheme = "eatvio"
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
let mainQueue = DispatchQueue.main
voipRegistry = PKPushRegistry(queue: mainQueue)
voipRegistry?.delegate = self
voipRegistry?.desiredPushTypes = [PKPushType.voIP]
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self
}
let launched = super.application(application, didFinishLaunchingWithOptions: launchOptions)
if let controller = window?.rootViewController as? FlutterViewController {
let channel = FlutterMethodChannel(
name: voipDiagnosticsChannelName,
binaryMessenger: controller.binaryMessenger
)
voipDiagnosticsChannel = channel
channel.setMethodCallHandler { [weak self] call, result in
guard let self else {
result(nil)
return
}
switch call.method {
case "drainVoipDiagnostics":
result(self.drainVoipDiagnostics())
case "drainPendingAcceptedVoipCall":
result(self.drainPendingAcceptedVoipCall())
default:
result(FlutterMethodNotImplemented)
}
}
}
return launched
} }
@available(iOS 8.0, *) @available(iOS 8.0, *)
func application( override func application(
_ application: UIApplication, continue userActivity: NSUserActivity, _ application: UIApplication,
restorationHandler: @escaping ([Any]?) -> Void continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool { ) -> Bool {
YXLSdk.shared.processUserActivity(userActivity) if YXLSdk.shared.processUserActivity(userActivity) {
return true return true
}
return super.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
} }
override public func application( override public func application(
_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] _ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool { ) -> Bool {
if url.scheme == appUrlScheme {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let queryItems = components?.queryItems ?? []
let conversationUuid = queryItems.first { $0.name == "conversationUuid" }?.value?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let callKitId = queryItems.first { $0.name == "callKitId" }?.value?
.trimmingCharacters(in: .whitespacesAndNewlines)
recordVoipDiagnostic(
"accepted_call_url_opened",
data: [
"callKitId": callKitId ?? "",
"hasCallKitId": callKitId != nil,
"conversationUuid": conversationUuid,
"url": url.absoluteString,
]
)
if let callKitId, !callKitId.isEmpty, !conversationUuid.isEmpty {
voipDiagnosticsChannel?.invokeMethod(
"acceptedVoipCall",
arguments: [
"id": callKitId,
"handle": conversationUuid,
"extra": [
"conversationUuid": conversationUuid
],
]
)
}
return true
}
return YXLSdk.shared.handleOpen( return YXLSdk.shared.handleOpen(
url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String) url,
sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String
)
} }
private func conversationUuid(fromActionUrl actionUrl: String?) -> String? {
guard
let actionUrl = actionUrl,
let components = URLComponents(string: actionUrl)
else {
return nil
}
let queryItems = components.queryItems ?? []
for key: String in ["uuid", "conversationUuid", "conversation_uuid"] {
if let value = queryItems.first(where: { $0.name == key })?.value,
!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
return value.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
let segments = components.path.split(separator: "/").map(String.init)
for index in segments.indices where segments[index] == "video-call" {
let nextIndex = segments.index(after: index)
if segments.indices.contains(nextIndex), !segments[nextIndex].isEmpty {
return segments[nextIndex]
}
}
return nil
}
private func conversationUuid(fromCall call: Call) -> String? {
let extra = call.data.extra as? [String: Any] ?? [:]
for key in ["conversationUuid", "conversation_uuid", "conversation_id", "chat_uuid", "uuid"] {
if let value = extra[key] as? String {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty {
return trimmed
}
}
}
let handle = call.data.handle.trimmingCharacters(in: .whitespacesAndNewlines)
if !handle.isEmpty {
return handle
}
return nil
}
private func storePendingAcceptedCall(_ call: Call) {
let payload = sanitizedDiagnosticValue(call.data.toJSON()) as? [String: Any] ?? [:]
UserDefaults.standard.set(payload, forKey: pendingAcceptedVoipCallKey)
}
private func drainPendingAcceptedVoipCall() -> [String: Any]? {
let defaults = UserDefaults.standard
guard let payload = defaults.dictionary(forKey: pendingAcceptedVoipCallKey) else {
return nil
}
defaults.removeObject(forKey: pendingAcceptedVoipCallKey)
return payload
}
private func bringAppToForeground() {
DispatchQueue.main.async {
for scene in UIApplication.shared.connectedScenes {
guard let windowScene = scene as? UIWindowScene else { continue }
for window in windowScene.windows {
window.makeKeyAndVisible()
}
}
self.window?.makeKeyAndVisible()
}
}
private func openAppForAcceptedCall(_ call: Call) {
guard let conversationUuid = conversationUuid(fromCall: call), !conversationUuid.isEmpty else {
recordVoipDiagnostic(
"accepted_call_missing_conversation_uuid",
data: ["callKitId": call.data.uuid]
)
return
}
var components = URLComponents()
components.scheme = appUrlScheme
components.host = "video-call"
components.queryItems = [
URLQueryItem(name: "callKitId", value: call.data.uuid),
URLQueryItem(name: "conversationUuid", value: conversationUuid)
]
guard let url = components.url else {
recordVoipDiagnostic(
"accepted_call_url_build_failed",
data: [
"callKitId": call.data.uuid,
"conversationUuid": conversationUuid,
]
)
return
}
recordVoipDiagnostic(
"accepted_call_open_app_requested",
data: [
"callKitId": call.data.uuid,
"conversationUuid": conversationUuid,
"url": url.absoluteString,
"appState": UIApplication.shared.applicationState.rawValue,
]
)
bringAppToForeground()
UIApplication.shared.open(url, options: [:]) { [weak self] success in
self?.recordVoipDiagnostic(
success ? "accepted_call_open_app_succeeded" : "accepted_call_open_app_failed",
data: [
"callKitId": call.data.uuid,
"conversationUuid": conversationUuid,
"url": url.absoluteString,
]
)
}
}
private func sanitizedDiagnosticValue(_ value: Any?) -> Any {
guard let value = value else {
return ""
}
if let string = value as? String {
return string
}
if let number = value as? NSNumber {
return number
}
if let bool = value as? Bool {
return bool
}
if let dictionary = value as? [AnyHashable: Any] {
return dictionary.reduce(into: [String: Any]()) { result, entry in
result[String(describing: entry.key)] = sanitizedDiagnosticValue(entry.value)
}
}
if let array = value as? [Any] {
return array.map { sanitizedDiagnosticValue($0) }
}
return String(describing: value)
}
private func recordVoipDiagnostic(_ name: String, data: [String: Any]) {
let defaults = UserDefaults.standard
var events = defaults.array(forKey: voipDiagnosticsStorageKey) as? [[String: Any]] ?? []
var event: [String: Any] = [
"name": name,
"timestamp": ISO8601DateFormatter().string(from: Date()),
"appState": UIApplication.shared.applicationState.rawValue,
]
event["data"] = sanitizedDiagnosticValue(data)
events.append(event)
if events.count > 20 {
events = Array(events.suffix(20))
}
defaults.set(events, forKey: voipDiagnosticsStorageKey)
voipDiagnosticsChannel?.invokeMethod("voipDiagnosticsUpdated", arguments: nil)
}
private func drainVoipDiagnostics() -> [[String: Any]] {
let defaults = UserDefaults.standard
let events = defaults.array(forKey: voipDiagnosticsStorageKey) as? [[String: Any]] ?? []
defaults.removeObject(forKey: voipDiagnosticsStorageKey)
return events
}
func pushRegistry(
_ registry: PKPushRegistry,
didUpdate credentials: PKPushCredentials,
for type: PKPushType
) {
let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
}
func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void
) {
guard type == .voIP else {
completion()
return
}
let dict = payload.dictionaryPayload
let pushMarkers = [
dict["type"] as? String,
dict["notification_type"] as? String,
dict["event"] as? String,
dict["action"] as? String,
dict["push_type"] as? String,
dict["call_type"] as? String,
]
.compactMap { $0?.lowercased().replacingOccurrences(of: "-", with: "_") }
if pushMarkers.contains(where: { $0.contains("video_call") })
&& pushMarkers.contains(where: { $0.contains("end") || $0.contains("cancel") }) {
recordVoipDiagnostic(
"voip_end_or_cancel_received",
data: [
"markers": pushMarkers,
"payload": dict,
]
)
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.endAllCalls()
completion()
return
}
let nameCaller = (dict["nameCaller"] as? String)
?? (dict["counterpartName"] as? String)
?? (dict["counterpart_name"] as? String)
?? (dict["caller_name"] as? String)
?? (dict["callerName"] as? String)
?? (dict["caller"] as? String)
?? ""
let conversationUuid = (dict["conversation_uuid"] as? String)
?? (dict["conversationUuid"] as? String)
?? (dict["conversation_id"] as? String)
?? (dict["chat_uuid"] as? String)
?? (dict["uuid"] as? String)
?? conversationUuid(fromActionUrl: dict["action_url"] as? String)
?? (dict["handle"] as? String)
?? ""
let callKitIdSource = [
(key: "call_id", value: dict["call_id"] as? String),
(key: "id", value: dict["id"] as? String),
(key: "message_id", value: dict["message_id"] as? String),
]
.first { item in
guard let trimmed = item.value?.trimmingCharacters(in: .whitespacesAndNewlines) else {
return false
}
return !trimmed.isEmpty && UUID(uuidString: trimmed) != nil
}
let id = callKitIdSource?.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? UUID().uuidString
let callKitIdSourceName = callKitIdSource?.key ?? "generated"
let isVideo = (dict["isVideo"] as? Bool)
?? (dict["is_video"] as? Bool)
?? true
var extra: [String: Any] = [:]
if !conversationUuid.isEmpty {
extra["conversationUuid"] = conversationUuid
}
if let avatar = dict["caller_avatar"] as? String ?? dict["avatar"] as? String
?? dict["avatarUrl"] as? String
?? dict["avatar_url"] as? String
?? dict["counterpartAvatarUrl"] as? String
?? dict["counterpart_avatar_url"] as? String
{
extra["counterpartAvatarUrl"] = avatar
}
if let messageId = dict["message_id"] as? String {
extra["messageId"] = messageId
}
if let roomName = dict["room_name"] as? String {
extra["roomName"] = roomName
}
if let localName = dict["localParticipantName"] as? String
?? dict["local_participant_name"] as? String
{
extra["localParticipantName"] = localName
}
if let counterpartName = dict["counterpartName"] as? String {
extra["counterpartName"] = counterpartName
} else if let counterpartName = dict["counterpart_name"] as? String {
extra["counterpartName"] = counterpartName
} else if !nameCaller.isEmpty {
extra["counterpartName"] = nameCaller
}
let data = flutter_callkit_incoming.Data(
id: id,
nameCaller: nameCaller,
handle: conversationUuid.isEmpty ? nameCaller : conversationUuid,
type: isVideo ? 1 : 0
)
data.extra = extra as NSDictionary
NSLog(
"[EatvioVoIP] report incoming call id=\(id) conversation=\(conversationUuid) state=\(UIApplication.shared.applicationState.rawValue) markers=\(pushMarkers.joined(separator: ",")) payload=\(dict)"
)
recordVoipDiagnostic(
"voip_incoming_received",
data: [
"callKitId": id,
"callKitIdSource": callKitIdSourceName,
"conversationUuid": conversationUuid,
"nameCaller": nameCaller,
"isVideo": isVideo,
"markers": pushMarkers,
"payload": dict,
]
)
guard let callkitPlugin = SwiftFlutterCallkitIncomingPlugin.sharedInstance else {
NSLog("[EatvioVoIP] CallKit plugin missing id=\(id)")
recordVoipDiagnostic(
"callkit_plugin_missing",
data: [
"callKitId": id,
"conversationUuid": conversationUuid,
]
)
completion()
return
}
callkitPlugin.showCallkitIncoming(
data,
fromPushKit: true
) {
let activeCalls = callkitPlugin.activeCalls()
let activeIds = activeCalls.compactMap { $0["id"] as? String }.joined(separator: ",")
NSLog(
"[EatvioVoIP] report incoming completed id=\(id) activeCallIds=\(activeIds)"
)
self.recordVoipDiagnostic(
"callkit_report_completed",
data: [
"callKitId": id,
"callKitIdSource": callKitIdSourceName,
"conversationUuid": conversationUuid,
"activeCallIds": activeIds,
"activeCallsCount": activeCalls.count,
]
)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let delayedActiveCalls = callkitPlugin.activeCalls()
let delayedActiveIds = delayedActiveCalls.compactMap { $0["id"] as? String }.joined(separator: ",")
self.recordVoipDiagnostic(
"callkit_active_calls_delayed",
data: [
"callKitId": id,
"callKitIdSource": callKitIdSourceName,
"conversationUuid": conversationUuid,
"activeCallIds": delayedActiveIds,
"activeCallsCount": delayedActiveCalls.count,
]
)
}
completion()
}
}
func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
storePendingAcceptedCall(call)
recordVoipDiagnostic(
"callkit_accept_native",
data: [
"callKitId": call.data.uuid,
"conversationUuid": conversationUuid(fromCall: call) ?? "",
"data": call.data.toJSON(),
]
)
voipDiagnosticsChannel?.invokeMethod("acceptedVoipCall", arguments: call.data.toJSON())
DispatchQueue.main.async { [weak self] in
self?.bringAppToForeground()
self?.openAppForAcceptedCall(call)
}
action.fulfill()
}
func onDecline(_ call: Call, _ action: CXEndCallAction) {
action.fulfill()
}
func onEnd(_ call: Call, _ action: CXEndCallAction) {
action.fulfill()
}
func onTimeOut(_ call: Call) {}
func didActivateAudioSession(_ audioSession: AVAudioSession) {}
func didDeactivateAudioSession(_ audioSession: AVAudioSession) {}
func providerDidReset() {}
} }

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>83</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
@@ -31,18 +31,22 @@
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSAppleMusicUsageDescription</key>
<string>This permission is not needed by the app, but it is required by an underlying API. If you see this dialog, contact us.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string> <string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
<key>NSMicrophoneUsageDescription</key>
<string>Нужен для звука в видеозвонках с консультантом</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Может использоваться для стабильного поддержания связи во время звонка.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string>Может использоваться для стабильного поддержания связи во время звонка.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Предоставление доступа позволит Вам добавлять фото к записям</string> <string>Предоставление доступа позволит Вам добавлять фото к записям</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string>
<string>voip</string>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
@@ -72,6 +76,57 @@
<string>yandexauth</string> <string>yandexauth</string>
<string>yandexauth2</string> <string>yandexauth2</string>
<string>yandexauth4</string> <string>yandexauth4</string>
<!-- SBP bank schemes (top ~50 by popularity, iOS 15+ caps at 50) -->
<string>bank100000000111</string>
<string>bank100000000004</string>
<string>bank100000000005</string>
<string>bank100000000008</string>
<string>bank100000000007</string>
<string>bank100000000015</string>
<string>bank100000000001</string>
<string>bank100000000010</string>
<string>bank100000000013</string>
<string>bank100000000012</string>
<string>bank100000000020</string>
<string>bank100000000025</string>
<string>bank100000000030</string>
<string>bank100000000030_new</string>
<string>bank100000000003</string>
<string>bank100000000043</string>
<string>bank100000000028</string>
<string>bank100000000086</string>
<string>bank100000000011</string>
<string>bank100000000044</string>
<string>bank100000000049</string>
<string>bank100000000500</string>
<string>bank100000000095</string>
<string>bank100000000900</string>
<string>bank100000000056</string>
<string>bank100000000053</string>
<string>bank100000000121</string>
<string>bank100000000082</string>
<string>bank100000000127</string>
<string>bank100000000017</string>
<string>bank100000000087</string>
<string>bank100000000052</string>
<string>bank100000000006</string>
<string>bank100000000098</string>
<string>bank100000000092</string>
<string>bank100000000229</string>
<string>bank100000000027</string>
<string>bank100000000080</string>
<string>bank100000000122</string>
<string>bank100000000124</string>
<string>bank100000000118</string>
<string>bank100000000159</string>
<string>bank100000000146</string>
<string>bank100000000069</string>
<string>bank100000000140</string>
<string>bank100000000047</string>
<string>bank100000000099</string>
<string>bank100000000135</string>
<string>bank100000000139</string>
<string>bank100000000022</string>
</array> </array>
<key>YAClientId</key> <key>YAClientId</key>
<string>5399b03636104c6d920b87c1ad329254</string> <string>5399b03636104c6d920b87c1ad329254</string>
@@ -85,6 +140,14 @@
<string>yx5399b03636104c6d920b87c1ad329254</string> <string>yx5399b03636104c6d920b87c1ad329254</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleURLName</key>
<string>Eatvio</string>
<key>CFBundleURLSchemes</key>
<array>
<string>eatvio</string>
</array>
</dict>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -48,18 +48,51 @@ platform :ios do
UI.message "Configuring Flutter for release mode..." UI.message "Configuring Flutter for release mode..."
sh("cd .. && flutter build ios --config-only --release") sh("cd .. && flutter build ios --config-only --release")
# Read build number from Flutter Generated.xcconfig
generated_xcconfig_path = File.join("..", "Flutter", "Generated.xcconfig")
flutter_build_number = nil
if File.exist?(generated_xcconfig_path)
File.foreach(generated_xcconfig_path) do |line|
matches = line.match(/FLUTTER_BUILD_NUMBER=(.*)/)
if matches
flutter_build_number = matches[1].strip
break
end
end
end
if flutter_build_number.nil?
UI.user_error!("Could not find FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}")
end
UI.message "Flutter build number: #{flutter_build_number}"
match(type: "appstore", readonly: true) match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "Runner.xcodeproj")
# Set build number explicitly instead of incrementing
increment_build_number(
xcodeproj: "Runner.xcodeproj",
build_number: flutter_build_number
)
update_code_signing_settings(
use_automatic_signing: false,
path: "Runner.xcodeproj",
team_id: "VL68C36HYS",
targets: ["Runner"],
code_sign_identity: "Apple Distribution",
profile_name: "match AppStore com.eatvioapp.app 1781098260"
)
build_app( build_app(
workspace: "Runner.xcworkspace", workspace: "Runner.xcworkspace",
scheme: "Runner", scheme: "Runner",
export_method: "app-store", export_method: "app-store",
codesigning_identity: "Apple Distribution", # codesigning_identity: "Apple Distribution",
export_team_id: "VL68C36HYS"
) )
app_store_connect_api_key( app_store_connect_api_key(
key_id: "325JS9JCZH", key_id: "325JS9JCZH",
issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3",
@@ -67,6 +100,7 @@ platform :ios do
duration: 1200, duration: 1200,
in_house: false in_house: false
) )
begin begin
upload_to_testflight( upload_to_testflight(
@@ -74,7 +108,7 @@ platform :ios do
skip_submission: true, skip_submission: true,
distribute_external: false, distribute_external: false,
notify_external_testers: false, notify_external_testers: false,
changelog: "TestFlight build #{get_build_number(xcodeproj: 'Runner.xcodeproj')}", changelog: "TestFlight build #{flutter_build_number}",
verbose: true verbose: true
) )
rescue => ex rescue => ex

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart'; import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -33,15 +34,17 @@ class AppConfig {
} }
/// Список глобальных BlocProvider'ов /// Список глобальных BlocProvider'ов
/// Используются lazy: true для отложенной инициализации, чтобы не блокировать старт приложения
List<BlocProvider> get globalBlocProviders => [ List<BlocProvider> get globalBlocProviders => [
BlocProvider<NewMessagesCubit>(create: (context) => sl<NewMessagesCubit>()), BlocProvider<VideoCallSessionCubit>.value(value: sl<VideoCallSessionCubit>()),
BlocProvider<NotificationsCubit>(create: (context) => sl<NotificationsCubit>()..getNotifications()), BlocProvider<NewMessagesCubit>(lazy: true, create: (context) => sl<NewMessagesCubit>()),
BlocProvider<FeedCubit>(create: (context) => sl<FeedCubit>()..loadFeedBlogPostList()), BlocProvider<NotificationsCubit>(lazy: true, create: (context) => sl<NotificationsCubit>()),
BlocProvider<ProfileAvatarCubit>(create: (context) => sl<ProfileAvatarCubit>()..getMyProfile()), BlocProvider<FeedCubit>(lazy: true, create: (context) => sl<FeedCubit>()),
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()), BlocProvider<ProfileAvatarCubit>(lazy: true, create: (context) => sl<ProfileAvatarCubit>()),
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()), 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<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()), BlocProvider<MarketingCubit>(lazy: true, create: (context) => sl<MarketingCubit>()),
]; ];
/// Делегаты локализации /// Делегаты локализации

View File

@@ -1,8 +1,12 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io' as io; import 'dart:io' as io;
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/env_config.dart'; import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/error_reporting/error_report_policy.dart';
import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart' as di;
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_fcm_handler.dart';
import 'package:eatvio_app/firebase_options.dart'; import 'package:eatvio_app/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
@@ -14,6 +18,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
/// Класс для инициализации приложения /// Класс для инициализации приложения
class AppInitializer { class AppInitializer {
/// Инициализация всех сервисов приложения /// Инициализация всех сервисов приложения
static Future<void> initialize({required AppBuildType buildType}) async { static Future<void> initialize({required AppBuildType buildType}) async {
await _initializeEnvironment(); await _initializeEnvironment();
@@ -37,9 +42,20 @@ class AppInitializer {
/// Инициализация Firebase /// Инициализация Firebase
static Future<void> _initializeFirebase() async { static Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('Firebase initialization timeout');
},
);
VideoCallFcmHandler.registerBackgroundHandler();
if (sl<AppConfig>().buildType == AppBuildType.prod) { if (sl<AppConfig>().buildType == AppBuildType.prod) {
await FirebaseMessaging.instance.requestPermission(); try {
await FirebaseMessaging.instance.requestPermission().timeout(const Duration(seconds: 5));
} catch (e) {
// Разрешение не критично, продолжаем работу
log('Failed to request Firebase messaging permission: $e');
}
} }
} }
@@ -67,6 +83,11 @@ class AppInitializer {
options.dsn = EnvConfig.sentryDsn; options.dsn = EnvConfig.sentryDsn;
options.tracesSampleRate = 1.0; options.tracesSampleRate = 1.0;
options.debug = false; options.debug = false;
options.beforeSend = (event, hint) {
if (kDebugMode) return null;
if (!ErrorReportPolicy.allowSentryEvent(event, hint)) return null;
return event;
};
}, appRunner: appRunner); }, appRunner: appRunner);
} }
} }

View File

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

View File

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

View File

@@ -7,18 +7,12 @@ class CommentsRepository {
CommentsRepository({required this.service}); CommentsRepository({required this.service});
Future<void> deleteComment({required int id}) async { Future<void> deleteComment({required int id}) async {
try { return service.deleteComment(id);
return service.deleteComment(id);
} catch (e) {
rethrow;
}
} }
Future<void> updateComment({required int id, required String body}) async { Future<void> updateComment({required int id, required String body}) async {
try { return service.updateComment(id, CommentUpdateModel(body: body));
return service.updateComment(id, CommentUpdateModel(body: body));
} catch (e) {
rethrow;
}
} }
} }

View File

@@ -9,4 +9,5 @@ const List<String> planTypeValues = ['fit', 'slim', 'wellness', 'behavior'];
const newMessagesChannelName = 'private-new-chat-messages'; const newMessagesChannelName = 'private-new-chat-messages';
const newNotificationsChannelName = 'private-message-events'; const newNotificationsChannelName = 'private-message-events';
const directChatChannelName = 'private-direct-chat'; const directChatChannelName = 'private-direct-chat';
const presenceDirectChatChannelName = 'presence-direct-chat';
const thresholdForPagination = 150; const thresholdForPagination = 150;

View File

@@ -69,4 +69,9 @@ class AppConstants {
static const String usersEndpoint = '/users'; static const String usersEndpoint = '/users';
static const String postsEndpoint = '/posts'; static const String postsEndpoint = '/posts';
static const String commentsEndpoint = '/comments'; static const String commentsEndpoint = '/comments';
// Delay of pending requests
/// 30 секунд стоит потому что запрос может идти 12-15 секунд
/// Особенно если производится отладка на сервере разработчика
static const Duration pendingDelay = Duration(seconds: 30);
} }

View File

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

View File

@@ -20,7 +20,7 @@ class StateUtils {
if (isError(state)) { if (isError(state)) {
// Попытка извлечь сообщение об ошибке из состояния // Попытка извлечь сообщение об ошибке из состояния
final stateString = state.toString(); final stateString = state.toString();
final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString); final errorMatch = RegExp(r'error[:\s]+([^,}]+)', caseSensitive: false).firstMatch(stateString);
return errorMatch?.group(1)?.trim(); return errorMatch?.group(1)?.trim();
} }
return null; return null;

View File

@@ -0,0 +1,16 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
abstract final class DeviceHwIdProvider {
static Future<String?> current() async {
final deviceInfo = DeviceInfoPlugin();
if (Platform.isIOS) {
return (await deviceInfo.iosInfo).identifierForVendor;
}
if (Platform.isAndroid) {
return (await deviceInfo.androidInfo).id;
}
return null;
}
}

View File

@@ -39,18 +39,21 @@ 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/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/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_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/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/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/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart'; import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_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/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -71,15 +74,27 @@ class CubitRegistrations {
sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl())); sl.registerLazySingleton<ProfileAvatarCubit>(() => ProfileAvatarCubit(profileRepository: sl()));
sl.registerLazySingleton<ChatsCubit>(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); sl.registerLazySingleton<ChatsCubit>(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl()));
sl.registerLazySingleton<InternetConnectionCubit>(() => InternetConnectionCubit()); sl.registerLazySingleton<InternetConnectionCubit>(() => InternetConnectionCubit());
sl.registerLazySingleton<VideoCallManager>(
() => VideoCallManager(
videoCallRepository: sl(),
profileRepository: sl(),
),
);
sl.registerLazySingleton<VideoCallSessionCubit>(
() => VideoCallSessionCubit(
videoCallManager: sl(),
systemPipCoordinator: sl(),
),
);
} }
/// Регистрация Cubit'ов как фабрик /// Регистрация Cubit'ов как фабрик
static void _registerFactoryCubits() { static void _registerFactoryCubits() {
// Auth related cubits // Auth related cubits
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl())); sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl())); sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl())); sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl(), prefs: sl()));
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl())); sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl())); sl.registerFactory<RegistrationCubitV2>(() => RegistrationCubitV2(registrationRepository: sl()));
// Feed related cubits // Feed related cubits
@@ -114,7 +129,7 @@ class CubitRegistrations {
// User content cubits // User content cubits
sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); sl.registerFactory<MyDietsCubit>(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl()));
sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl())); sl.registerFactory<MyPlansCubit>(() => MyPlansCubit(myPlansRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl(), recipeRepository: sl()));
// Other feature cubits // Other feature cubits
sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl())); sl.registerFactory<CalendarCubit>(() => CalendarCubit(calendarRepository: sl()));
@@ -135,6 +150,9 @@ class CubitRegistrations {
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl())); sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl())); sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl())); sl.registerFactory<OrderInfoCubit>(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()));
sl.registerFactory<ConsultationPaymentCubit>(
() => ConsultationPaymentCubit(paymentRepository: sl(), sbpRepository: sl()),
);
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl())); sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl())); sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl())); sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));

View File

@@ -1,10 +1,15 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/constants/app_constants.dart';
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:eatvio_app/core/error_reporting/sentry_error_reporter.dart';
import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart'; import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart';
import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/prefs.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:sentry_dio/sentry_dio.dart';
/// Регистрация внешних зависимостей в системе внедрения зависимостей /// Регистрация внешних зависимостей в системе внедрения зависимостей
class ExternalRegistrations { class ExternalRegistrations {
@@ -12,6 +17,7 @@ class ExternalRegistrations {
/// Регистрация всех внешних зависимостей /// Регистрация всех внешних зависимостей
static void registerAll() { static void registerAll() {
sl.registerLazySingleton<ErrorReporter>(SentryErrorReporter.new);
_registerDio(); _registerDio();
_registerPrefs(); _registerPrefs();
} }
@@ -19,14 +25,23 @@ class ExternalRegistrations {
/// Регистрация Dio HTTP клиента /// Регистрация Dio HTTP клиента
static void _registerDio() { static void _registerDio() {
sl.registerLazySingleton( sl.registerLazySingleton(
() => () => Dio(
Dio(BaseOptions(baseUrl: sl<AppConfig>().baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) BaseOptions(
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), baseUrl: sl<AppConfig>().baseUrl,
headers: {'accept': 'application/json', 'content-type': 'application/json'},
connectTimeout: AppConstants.pendingDelay,
receiveTimeout: AppConstants.pendingDelay,
sendTimeout: AppConstants.pendingDelay,
),
)..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()])
..addSentry(
failedRequestStatusCodes: const [SentryStatusCode.defaultRange()],
),
); );
} }
/// Регистрация Prefs /// Регистрация Prefs
static void _registerPrefs() { static void _registerPrefs() {
sl.registerSingletonAsync<Prefs>(()async => Prefs(await SharedPreferences.getInstance())); sl.registerSingletonAsync<Prefs>(() async => Prefs(await SharedPreferences.getInstance()));
} }
} }

View File

@@ -17,12 +17,15 @@ import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
import 'package:eatvio_app/features/barcode/repositories/barcode_repository.dart';
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_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/notifications/repository/notifications_repository.dart';
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_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/plan_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
@@ -32,6 +35,7 @@ import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.da
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart'; import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:eatvio_app/features/video_calls/repositories/video_call_repository.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
/// Регистрация всех Repository в системе внедрения зависимостей /// Регистрация всех Repository в системе внедрения зависимостей
@@ -48,6 +52,7 @@ class RepositoryRegistrations {
static void _registerSingletonRepositories() { static void _registerSingletonRepositories() {
// Core repositories // Core repositories
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl())); sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
sl.registerLazySingleton<VideoCallRepository>(() => VideoCallRepository(videoCallService: sl()));
sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl())); sl.registerLazySingleton<FeedRepository>(() => FeedRepository(feedService: sl()));
sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl())); sl.registerLazySingleton<ProfileRepository>(() => ProfileRepository(profileService: sl()));
sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl())); sl.registerLazySingleton<CalendarRepository>(() => CalendarRepository(calendarService: sl()));
@@ -55,6 +60,7 @@ class RepositoryRegistrations {
sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl())); sl.registerLazySingleton<ConsultantRepository>(() => ConsultantRepository(consultantService: sl()));
sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl())); sl.registerLazySingleton<DiaryRepository>(() => DiaryRepository(diaryService: sl()));
sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl())); sl.registerLazySingleton<GlossaryRepository>(() => GlossaryRepository(glossaryService: sl()));
sl.registerLazySingleton<BarcodeRepository>(() => BarcodeRepository(barcodeService: sl(), dio: sl()));
sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl())); sl.registerLazySingleton<DietRepository>(() => DietRepository(dietsService: sl()));
sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl())); sl.registerLazySingleton<PlansRepository>(() => PlansRepository(plansService: sl()));
sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl())); sl.registerLazySingleton<PlanRepository>(() => PlanRepository(planService: sl()));
@@ -70,6 +76,8 @@ class RepositoryRegistrations {
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl())); sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl())); sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl())); sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerLazySingleton<SbpRepository>(() => SbpRepository(sbpService: sl()));
sl.registerLazySingleton<PaymentRepository>(() => PaymentRepository(chatService: sl()));
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl())); sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl())); sl.registerLazySingleton<RegistrationRepositoryV2>(() => RegistrationRepositoryV2(registrationService: sl()));

View File

@@ -19,12 +19,14 @@ import 'package:eatvio_app/features/diary/services/diary_service.dart';
import 'package:eatvio_app/features/diets/services/diet_service.dart'; import 'package:eatvio_app/features/diets/services/diet_service.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart'; import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; import 'package:eatvio_app/features/glossary/services/glossary_service.dart';
import 'package:eatvio_app/features/barcode/services/barcode_service.dart';
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_finance/services/cards_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/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:eatvio_app/features/payment/services/sbp_service.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_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/plan_service.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart'; import 'package:eatvio_app/features/plans/services/plans_service.dart';
@@ -34,6 +36,9 @@ import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart'; import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart'; import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart'; import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_callkit_coordinator.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_service.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_system_pip_coordinator.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
/// Регистрация всех Service в системе внедрения зависимостей /// Регистрация всех Service в системе внедрения зависимостей
@@ -49,6 +54,7 @@ class ServiceRegistrations {
static void _registerSingletonServices() { static void _registerSingletonServices() {
// Core services // Core services
sl.registerLazySingleton<ChatService>(() => ChatService(sl())); sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
sl.registerLazySingleton<VideoCallService>(() => VideoCallService(sl()));
sl.registerLazySingleton<FeedService>(() => FeedService(sl())); sl.registerLazySingleton<FeedService>(() => FeedService(sl()));
sl.registerLazySingleton<ProfileService>(() => ProfileService(sl())); sl.registerLazySingleton<ProfileService>(() => ProfileService(sl()));
sl.registerLazySingleton<CalendarService>(() => CalendarService(sl())); sl.registerLazySingleton<CalendarService>(() => CalendarService(sl()));
@@ -56,6 +62,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl())); sl.registerLazySingleton<ConsultantService>(() => ConsultantService(sl()));
sl.registerLazySingleton<DiaryService>(() => DiaryService(sl())); sl.registerLazySingleton<DiaryService>(() => DiaryService(sl()));
sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl())); sl.registerLazySingleton<GlossaryService>(() => GlossaryService(sl()));
sl.registerLazySingleton<BarcodeService>(() => BarcodeService(sl()));
sl.registerLazySingleton<DietService>(() => DietService(sl())); sl.registerLazySingleton<DietService>(() => DietService(sl()));
sl.registerLazySingleton<PlansService>(() => PlansService(sl())); sl.registerLazySingleton<PlansService>(() => PlansService(sl()));
sl.registerLazySingleton<PlanService>(() => PlanService(sl())); sl.registerLazySingleton<PlanService>(() => PlanService(sl()));
@@ -70,7 +77,13 @@ class ServiceRegistrations {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl())); sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl())); sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl())); sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi()); sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi(errorReporter: sl()));
sl.registerLazySingleton<VideoCallCallKitCoordinator>(
() => VideoCallCallKitCoordinator(firebaseApi: sl()),
);
sl.registerLazySingleton<VideoCallSystemPipCoordinator>(
() => VideoCallSystemPipCoordinator(),
);
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl())); sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl())); sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl())); sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
@@ -78,6 +91,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl())); sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl())); sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl())); sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
sl.registerLazySingleton<SbpService>(() => const SbpServiceImpl());
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl())); sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl())); sl.registerLazySingleton<RegistrationServiceV2>(() => RegistrationServiceV2(sl()));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,48 @@
import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart' show MethodChannel, PlatformException; import 'package:flutter/services.dart' show MethodChannel, PlatformException;
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
class FirebaseApi { class FirebaseApi {
final ErrorReporter _errorReporter;
final _firebaseMessaging = FirebaseMessaging.instance; final _firebaseMessaging = FirebaseMessaging.instance;
FirebaseApi({required ErrorReporter errorReporter}) : _errorReporter = errorReporter;
Future<String?> getToken() async { Future<String?> getToken() async {
String? token; String? token;
if (await _isHuaweiDevice()) { if (await _isHuaweiDevice()) {
token = await _getHuaweiPushTokenNative(); token = await _getHuaweiPushTokenNative().timeout(
const Duration(seconds: 5),
onTimeout: () => null,
);
} else { } else {
if ((sl<AppConfig>().buildType == AppBuildType.prod)) { if ((sl<AppConfig>().buildType == AppBuildType.prod)) {
await _firebaseMessaging.requestPermission(); try {
await _firebaseMessaging.requestPermission().timeout(
const Duration(seconds: 5),
);
} catch (e) {
// Разрешение не критично, продолжаем
log('Failed to request Firebase messaging permission: $e');
}
} }
token = await _firebaseMessaging.getToken(); token = await _firebaseMessaging.getToken().timeout(
const Duration(seconds: 5),
onTimeout: () => null,
);
} }
return token; return token;
} }
@@ -35,13 +53,20 @@ class FirebaseApi {
await sl<Dio>().post( await sl<Dio>().post(
'/api/anonymous_token', '/api/anonymous_token',
data: { data: {
'platform': Platform.isIOS ? 'ios' : 'android', 'platform': Platform.isIOS ? 'ios' : (await _isHuaweiDevice()) ? 'huawei' : 'android',
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(), 'token': await getToken(),
}, },
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('registerAnonymousPushToken timeout');
},
); );
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция
} }
} }
@@ -57,13 +82,105 @@ class FirebaseApi {
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
'token': await getToken(), 'token': await getToken(),
}, },
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('registerPushToken timeout');
},
); );
if (Platform.isIOS) {
await _registerIosVoipPushToken();
}
} catch (e, st) { } catch (e, st) {
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
// Не пробрасываем ошибку дальше, так как это не критичная операция
} }
} }
} }
Future<void> unregisterPushTokens() async {
if (!await TokenStorage.haveTokenInStorage()) return;
try {
await unregisterPushToken();
} catch (_) {}
if (Platform.isIOS) {
try {
await _unregisterIosVoipPushToken();
} catch (_) {}
}
}
Future<void> unregisterPushToken() async {
if (!await TokenStorage.haveTokenInStorage()) return;
try {
final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false;
final platform = Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android');
final token = (await getToken()) ?? '';
await sl<Dio>().post(
'/api/profile/notification/token/unregister',
data: {
'platform': platform,
'token': token,
},
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('unregisterPushToken timeout');
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
}
}
Future<void> _registerIosVoipPushToken() async {
try {
final token = (await FlutterCallkitIncoming.getDevicePushTokenVoIP())?.trim();
if (token == null || token.isEmpty) return;
final deviceInfo = DeviceInfoPlugin();
await sl<Dio>().post(
'/api/profile/notification/token/voip/register',
data: {
'platform': 'ios',
'hw_id': (await deviceInfo.iosInfo).identifierForVendor,
'token': token,
},
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('registerPushToken voip timeout');
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
}
}
Future<void> _unregisterIosVoipPushToken() async {
try {
final token = (await FlutterCallkitIncoming.getDevicePushTokenVoIP())?.trim() ?? '';
await sl<Dio>().post(
'/api/profile/notification/token/voip/unregister',
data: {
'token': token,
},
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('unregisterPushToken voip timeout');
},
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
}
}
Future<bool> _isHuaweiDevice() async { Future<bool> _isHuaweiDevice() async {
if (!Platform.isAndroid) return false; if (!Platform.isAndroid) return false;
try { try {
@@ -84,10 +201,12 @@ class FirebaseApi {
} on PlatformException catch (e, st) { } on PlatformException catch (e, st) {
showErrorToast('$e $st'); showErrorToast('$e $st');
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null; return null;
} catch (e, st) { } catch (e, st) {
showErrorToast('$e $st'); showErrorToast('$e $st');
log(e.toString(), stackTrace: st); log(e.toString(), stackTrace: st);
await _errorReporter.report(e, st, origin: 'FirebaseApi');
return null; return null;
} }
} }

View File

@@ -1,6 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
abstract class ImageCompressUtility { abstract class ImageCompressUtility {
@@ -27,33 +29,21 @@ abstract class ImageCompressUtility {
} }
static Future<File> compressFile(File file) async { static Future<File> compressFile(File file) async {
final image = img.decodeImage(file.readAsBytesSync())!; try {
int width = image.width; final appConfig = sl<AppConfig>();
int height = image.height; if (appConfig.buildType == AppBuildType.tests) {
return file;
}
} catch (_) {}
if (width > height) { final compressedPath = '${p.withoutExtension(file.path)}_compressed.jpg';
if (width > 1080) {
height = (height * 1080) ~/ width; final result = await FlutterImageCompress.compressAndGetFile(file.absolute.path, compressedPath, quality: 90, minHeight: 1080, minWidth: 1080);
width = 1080;
} if (result == null) {
} else { return file;
if (height > 1080) {
width = (width * 1080) ~/ height;
height = 1080;
}
} }
String filePathWithoutExtension = p.withoutExtension(file.path); return File(result.path);
String compressedFilePath = '${filePathWithoutExtension}_compressed.jpg';
final result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
compressedFilePath,
quality: 90,
minHeight: height,
minWidth: width,
);
return File(result!.path);
} }
} }

View File

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

View File

@@ -1,6 +1,8 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class TokenInterceptor extends Interceptor { class TokenInterceptor extends Interceptor {
@@ -23,6 +25,7 @@ class TokenInterceptor extends Interceptor {
ErrorInterceptorHandler handler, ErrorInterceptorHandler handler,
) async { ) async {
if (err.response?.statusCode == 401) { if (err.response?.statusCode == 401) {
await sl<WsManager>().clear();
await TokenStorage.removeTokenInStorage(); await TokenStorage.removeTokenInStorage();
AppRouter.navigatorKey.currentContext?.go('/auth-screen'); AppRouter.navigatorKey.currentContext?.go('/auth-screen');
} }

View File

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

View File

@@ -4,6 +4,7 @@ import 'package:eatvio_app/core/di/cubit_registrations.dart';
import 'package:eatvio_app/core/di/external_registrations.dart'; import 'package:eatvio_app/core/di/external_registrations.dart';
import 'package:eatvio_app/core/di/repository_registrations.dart'; import 'package:eatvio_app/core/di/repository_registrations.dart';
import 'package:eatvio_app/core/di/service_registrations.dart'; import 'package:eatvio_app/core/di/service_registrations.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -18,6 +19,9 @@ Future<void> init({required AppBuildType buildType}) async {
// Регистрация внешних зависимостей (Dio, etc.) // Регистрация внешних зависимостей (Dio, etc.)
ExternalRegistrations.registerAll(); ExternalRegistrations.registerAll();
// Ожидание инициализации асинхронных зависимостей
await sl.getAsync<Prefs>();
// Регистрация сервисов // Регистрация сервисов
ServiceRegistrations.registerAll(); ServiceRegistrations.registerAll();
@@ -28,5 +32,5 @@ Future<void> init({required AppBuildType buildType}) async {
CubitRegistrations.registerAll(); CubitRegistrations.registerAll();
// Регистрация WebSocket менеджера // Регистрация WebSocket менеджера
sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl())); sl.registerLazySingleton<WsManager>(() => WsManager(wsService: sl(), errorReporter: sl()));
} }

View File

@@ -0,0 +1,101 @@
import 'dart:developer' as developer;
import 'dart:async';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_ended_push_handler.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_ended_push_payload.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_notification_navigator.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_push_payload.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class PushNavigationHandler {
static bool _initialized = false;
static const MethodChannel _hmsChannel = MethodChannel('com.eatvioapp/hms_push');
static Future<void> initialize() async {
if (_initialized) return;
_initialized = true;
try {
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleMessageInternal(initialMessage, source: 'initialMessage');
});
}
} catch (e) {
developer.log('PushNavigationHandler: error while getting initialMessage: $e', name: 'PushNavigationHandler');
}
FirebaseMessaging.onMessageOpenedApp.listen((message) => _handleMessageInternal(message, source: 'onMessageOpenedApp'));
_hmsChannel.setMethodCallHandler((call) async {
if (call.method == 'openActionUrl') {
final url = call.arguments as String? ?? '';
handleActionUrlFromNative(url);
}
});
}
static void handleActionUrlFromNative(String url) {
if (url.isEmpty) {
developer.log('PushNavigationHandler.handleActionUrlFromNative: empty url', name: 'PushNavigationHandler');
return;
}
developer.log('PushNavigationHandler.handleActionUrlFromNative: navigating by url=$url', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(url);
}
static void handleMessage(RemoteMessage message) {
_handleMessageInternal(message, source: 'manualHandle');
}
static void _handleMessageInternal(RemoteMessage message, {required String source}) {
final data = Map<String, dynamic>.from(message.data);
final endedPayload = VideoCallEndedPushPayload.tryParse(data);
if (endedPayload != null) {
developer.log(
'PushNavigationHandler: dismiss incoming call UI, source=$source',
name: 'PushNavigationHandler',
);
unawaited(VideoCallEndedPushHandler.dismissIncomingCallUi());
return;
}
final videoCallPayload = VideoCallPushPayload.tryParse(data);
if (videoCallPayload != null) {
developer.log(
'PushNavigationHandler: opening incoming video call, source=$source, conversation=${videoCallPayload.conversationUuid}',
name: 'PushNavigationHandler',
);
unawaited(
VideoCallNotificationNavigator.openIncomingCall(
conversationUuid: videoCallPayload.conversationUuid,
counterpartName: videoCallPayload.counterpartName,
counterpartAvatarUrl: videoCallPayload.counterpartAvatarUrl,
localParticipantName: videoCallPayload.localParticipantName,
counterpartIsConsultant: videoCallPayload.counterpartIsConsultant,
entryMode: VideoCallEntryMode.incomingAccepted,
),
);
return;
}
final actionUrl = data['action_url'] as String?;
if (actionUrl == null || actionUrl.isEmpty) {
developer.log('PushNavigationHandler: no action_url in message data, source=$source, data=$data', name: 'PushNavigationHandler');
return;
}
developer.log('PushNavigationHandler: navigating by action_url=$actionUrl, source=$source', name: 'PushNavigationHandler');
AppRouter.navigateByDeepLinkUrl(actionUrl);
}
}

View File

@@ -1 +1 @@
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown } enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown, isEmailVerificationSkipped }

View File

@@ -1,5 +1,3 @@
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
@@ -12,19 +10,14 @@ import 'package:eatvio_app/features/registration/presentation/pages/verify_email
import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart'; import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
/// Маршруты для аутентификации
class AuthRoutes { class AuthRoutes {
/// Создание маршрутов аутентификации
static List<RouteBase> createRoutes() { static List<RouteBase> createRoutes() {
return [ return [
GoRoute( GoRoute(
path: RouteConstants.authScreen, path: RouteConstants.authScreen,
name: 'authScreen', name: 'authScreen',
builder: (context, state) => const AuthScreen(), builder: (context, state) => const AuthScreen(),
redirect: (context, state) {
sl<FirebaseApi>().registerAnonymousPushToken();
return null;
},
routes: [ routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()), GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),

View File

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

View File

@@ -1,6 +1,7 @@
import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_nav_scope.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart';
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
@@ -23,14 +24,16 @@ class MainNavigationRoutes {
return [ return [
ShellRoute( ShellRoute(
builder: (context, state, child) { builder: (context, state, child) {
return BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>( // Инициализация ProfileAvatarCubit при первом использовании ShellRoute
builder: (BuildContext context, ProfileAvatarState state) { final profileAvatarCubit = context.read<ProfileAvatarCubit>();
if (state is ProfileAvatarStateLoaded) { if (profileAvatarCubit.state is ProfileAvatarStateLoading) {
return BottomNavigationBarWidget(profileModel: state.profile, child: child); // Вызываем getMyProfile() только если состояние loading (первый запуск)
} WidgetsBinding.instance.addPostFrameCallback((_) {
return const SizedBox.shrink(); profileAvatarCubit.getMyProfile();
}, });
); }
return _MainShellWithNavScope(currentLocation: state.matchedLocation, child: child);
}, },
routes: [ routes: [
GoRoute( GoRoute(
@@ -43,7 +46,14 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.feedScreen, path: RouteConstants.feedScreen,
name: 'feedScreen', name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), pageBuilder: (context, state) {
final initialTabIndexFromQuery = int.tryParse(state.uri.queryParameters['initialTabIndex'] ?? '');
final extra = state.extra;
if (extra is int) {
return NoTransitionPage(child: FeedScreen(initialTabIndex: extra));
}
return NoTransitionPage(child: FeedScreen(initialTabIndex: initialTabIndexFromQuery));
},
redirect: (context, state) async { redirect: (context, state) async {
try { try {
await sl<WsManager>().initialize(); await sl<WsManager>().initialize();
@@ -56,7 +66,7 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.diaryScreen, path: RouteConstants.diaryScreen,
name: 'diaryScreen', name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), pageBuilder: (context, state) => NoTransitionPage(child: DiaryScreen(date: state.extra as DateTime?)),
), ),
GoRoute( GoRoute(
path: ConsultationsScreen.path, path: ConsultationsScreen.path,
@@ -66,7 +76,10 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.chatSelectScreen, path: RouteConstants.chatSelectScreen,
name: 'chatSelectScreen', name: 'chatSelectScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), pageBuilder: (context, state) {
final initialSearchQuery = state.extra is String ? state.extra as String : null;
return NoTransitionPage(child: ChatsScreen(initialSearchQuery: initialSearchQuery));
},
), ),
GoRoute( GoRoute(
path: RouteConstants.notificationsScreen, path: RouteConstants.notificationsScreen,
@@ -76,19 +89,34 @@ class MainNavigationRoutes {
GoRoute( GoRoute(
path: RouteConstants.menuScreen, path: RouteConstants.menuScreen,
name: 'menuScreen', name: 'menuScreen',
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) => const NoTransitionPage(child: MenuScreen()),
child: BlocBuilder<ProfileAvatarCubit, ProfileAvatarState>(
builder: (BuildContext context, ProfileAvatarState state) {
if (state is ProfileAvatarStateLoaded) {
return MenuScreen(profileModel: state.profile);
}
return const SizedBox.shrink();
},
),
),
), ),
], ],
), ),
]; ];
} }
} }
/// Wraps the shell with [BottomNavScope] so descendants (e.g. notifications)
/// can switch tabs via [BottomNavController.switchToRoute] instead of pushing.
class _MainShellWithNavScope extends StatefulWidget {
const _MainShellWithNavScope({required this.child, required this.currentLocation});
final Widget child;
final String currentLocation;
@override
State<_MainShellWithNavScope> createState() => _MainShellWithNavScopeState();
}
class _MainShellWithNavScopeState extends State<_MainShellWithNavScope> {
final BottomNavController _navController = BottomNavController();
@override
Widget build(BuildContext context) {
return BottomNavScope(
controller: _navController,
child: BottomNavigationBarWidget(currentLocation: widget.currentLocation, child: widget.child),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/firebase_api.dart';
@@ -6,12 +7,15 @@ import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart'; import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart';
import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart';
import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
import 'package:eatvio_app/features/video_calls/presentation/pages/video_call_screen.dart';
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
@@ -58,24 +62,33 @@ class OtherRoutes {
builder: (context, state) => const WelcomeScreen(), builder: (context, state) => const WelcomeScreen(),
redirect: (context, state) async { redirect: (context, state) async {
try { try {
final prefs = sl<Prefs>(); // Prefs уже инициализирован в locator_service, но на всякий случай проверяем
final firebaseApi = sl<FirebaseApi>(); final prefs = await sl.getAsync<Prefs>();
final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false; final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false;
await firebaseApi.registerPushToken();
if (!welcomeScreenWasShowed) { if (!welcomeScreenWasShowed) {
return null; return null;
} else if (await TokenStorage.haveTokenInStorage()) { }
return '/feed-screen';
} else { if (await TokenStorage.haveTokenInStorage()) {
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; try {
if (!hasAnonymousTokenBeenSent) { final profileRepository = sl<ProfileRepository>();
await firebaseApi.registerAnonymousPushToken(); final profile = await profileRepository.getMyProfile();
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
final firebaseApi = sl<FirebaseApi>();
await firebaseApi.registerPushToken();
if (profile.emailVerifiedAt != null || (prefs.getBool(PrefsKeys.isEmailVerificationSkipped) ?? false)) {
return RouteConstants.feedScreen;
}
return RouteConstants.verifyEmailScreen;
} catch (e) {
return '/auth-screen';
} }
} else {
return '/auth-screen'; return '/auth-screen';
} }
} catch (e) { } catch (e) {
return '/feed-screen'; return '/auth-screen';
} }
}, },
), ),
@@ -85,7 +98,27 @@ class OtherRoutes {
builder: (context, state) => const UpdateScreen(), builder: (context, state) => const UpdateScreen(),
redirect: (context, state) async { redirect: (context, state) async {
try { try {
final versions = (await sl<Dio>().get('${sl<AppConfig>().baseUrl}/api/app_versions')).data; // Проверка версии с таймаутом, чтобы не блокировать запуск приложения
final versions = await sl<Dio>()
.get('${sl<AppConfig>().baseUrl}/api/app_versions')
.timeout(
const Duration(seconds: 5),
onTimeout: () {
// При таймауте пропускаем проверку и идем дальше
throw TimeoutException('Version check timeout');
},
)
.then((response) => response.data)
.catchError((e) {
// При любой ошибке пропускаем проверку версии
return null;
});
if (versions == null) {
// Если проверка не удалась, пропускаем экран обновления
return RouteConstants.welcomeScreen;
}
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
String version = packageInfo.version; String version = packageInfo.version;
Version currentVersion = Version.parse(version); Version currentVersion = Version.parse(version);
@@ -96,6 +129,7 @@ class OtherRoutes {
return RouteConstants.welcomeScreen; return RouteConstants.welcomeScreen;
} }
} catch (e) { } catch (e) {
// При любой ошибке пропускаем экран обновления
return RouteConstants.welcomeScreen; return RouteConstants.welcomeScreen;
} }
}, },
@@ -121,22 +155,32 @@ class OtherRoutes {
GoRoute( GoRoute(
path: RecipeCreateScreen.path, path: RecipeCreateScreen.path,
name: RecipeCreateScreen.name, name: RecipeCreateScreen.name,
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), builder: (context, state) {
if (state.extra is Map<String, dynamic>) {
final data = state.extra as Map<String, dynamic>;
return RecipeCreateScreen(recipe: data['recipe'] as Recipe?, autoValidate: data['autoValidate'] as bool? ?? false);
}
return RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null);
},
), ),
GoRoute( GoRoute(
path: CreateDietScreen.path, path: CreateDietScreen.path,
name: CreateDietScreen.name, name: CreateDietScreen.name,
builder: (context, state) { builder: (context, state) {
final diet = state.extra as Diet?; final data = state.extra as Map<String, dynamic>?;
return CreateDietScreen(diet: diet); final diet = data?['diet'] as Diet?;
final isEditing = data?['isEditing'] as bool?;
return CreateDietScreen(diet: diet, isEditing: isEditing);
}, },
), ),
GoRoute( GoRoute(
path: DietPreviewScreen.path, path: DietPreviewScreen.path,
name: DietPreviewScreen.name, name: DietPreviewScreen.name,
builder: (context, state) { builder: (context, state) {
final data = state.extra as Diet; final data = state.extra as Map<String, dynamic>;
return DietPreviewScreen(diet: data); final diet = data['diet'] as Diet;
final isImageInBase64 = data['isImageInBase64'] as bool;
return DietPreviewScreen(diet: diet, isImageInBase64: isImageInBase64);
}, },
), ),
GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()), GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()),
@@ -159,7 +203,7 @@ class OtherRoutes {
GoRoute( GoRoute(
path: '${RouteConstants.subsScreen}/:slug', path: '${RouteConstants.subsScreen}/:slug',
name: 'subsScreen', name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), builder: (context, state) => SubsScreen(slug: state.pathParameters['slug'], subType: state.extra as SubType),
), ),
// Chat routes // Chat routes
@@ -167,10 +211,24 @@ class OtherRoutes {
path: RouteConstants.chatScreen, path: RouteConstants.chatScreen,
name: 'chatScreen', name: 'chatScreen',
builder: (context, state) { builder: (context, state) {
final uuidFromQuery = state.uri.queryParameters['uuid'];
final orderIdFromQuery = state.uri.queryParameters['orderId'];
if (uuidFromQuery != null || orderIdFromQuery != null) {
return ChatScreen(
uuid: uuidFromQuery,
orderId: orderIdFromQuery,
sendInitialMessage: false,
);
}
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String; final uuid = data['uuid'] as String?;
final orderId = data['orderId'] as String?;
final sendInitialMessage = data['sendInitialMessage'] as bool?; final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); return ChatScreen(
uuid: uuid,
orderId: orderId,
sendInitialMessage: sendInitialMessage,
);
}, },
), ),
GoRoute( GoRoute(
@@ -178,13 +236,45 @@ class OtherRoutes {
name: 'emptyChatScreen', name: 'emptyChatScreen',
builder: (context, state) => EmptyChatScreen(user: state.extra as ProfileModel), builder: (context, state) => EmptyChatScreen(user: state.extra as ProfileModel),
), ),
GoRoute(
path: RouteConstants.videoCallScreen,
name: VideoCallScreen.name,
builder: (context, state) {
final extra = state.extra;
if (extra is Map<String, dynamic>) {
return VideoCallScreen(
conversationUuid: extra['conversationUuid'] as String,
counterpartName: extra['counterpartName'] as String?,
counterpartAvatarUrl: extra['counterpartAvatarUrl'] as String?,
counterpartUserId: extra['counterpartUserId'] as int?,
counterpartIsOnline: extra['counterpartIsOnline'] as bool?,
counterpartLastSeenText: extra['counterpartLastSeenText'] as String?,
counterpartIsConsultant: extra['counterpartIsConsultant'] as bool?,
localParticipantName: extra['localParticipantName'] as String?,
entryMode: _videoCallEntryModeFromRouteExtra(extra['entryMode']),
onBackToChat: extra['onBackToChat'] as void Function()?,
onHangUp: extra['onHangUp'] as void Function()?,
);
}
final uuid = state.uri.queryParameters['uuid'];
if (uuid != null) {
return VideoCallScreen(conversationUuid: uuid);
}
throw ArgumentError('VideoCallScreen: pass extra Map with conversationUuid or query uuid');
},
),
// Consultant routes // Consultant routes
GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
GoRoute( GoRoute(
path: RouteConstants.stepperBecomeConsultantScreen, path: RouteConstants.stepperBecomeConsultantScreen,
name: 'stepperBecomeConsultantScreen', name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
final slug = data['slug'] as String?;
final isCanceled = data['isCanceled'] as bool?;
return StepperBecomeConsultantScreen(slug: slug, isCanceled: isCanceled);
},
), ),
GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()), GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()),
@@ -217,6 +307,11 @@ class OtherRoutes {
path: InfoByOrderScreen.path, path: InfoByOrderScreen.path,
name: InfoByOrderScreen.name, name: InfoByOrderScreen.name,
builder: (context, state) { builder: (context, state) {
final queryParams = state.uri.queryParameters;
final planIdFromQuery = int.tryParse(queryParams['planId'] ?? '');
if (planIdFromQuery != null) {
return InfoByOrderScreen(planId: planIdFromQuery, successfulPayment: null, customerSlug: null, isConsultantView: true);
}
final data = state.extra as Map<String, dynamic>; final data = state.extra as Map<String, dynamic>;
final planId = data['planId'] as int; final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?; final successfulPayment = data['successfulPayment'] as bool?;
@@ -240,15 +335,22 @@ class OtherRoutes {
builder: (context, state) { builder: (context, state) {
final data = state.extra as Map<String, int>; final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int; final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int; return UserDiaryScreen(diaryId: diaryId);
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
}, },
), ),
GoRoute( GoRoute(
path: PersonalDietSuggestionScreen.path, path: PersonalDietSuggestionScreen.path,
name: PersonalDietSuggestionScreen.name, name: PersonalDietSuggestionScreen.name,
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,), builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int),
), ),
]; ];
} }
} }
VideoCallEntryMode _videoCallEntryModeFromRouteExtra(Object? raw) {
if (raw is VideoCallEntryMode) return raw;
if (raw == VideoCallEntryMode.incomingAccepted.name) {
return VideoCallEntryMode.incomingAccepted;
}
return VideoCallEntryMode.outgoing;
}

View File

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

View File

@@ -80,6 +80,48 @@ class CustomTextStyles {
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);
static TextStyle get videoCallFlowPlaceholderTitle => theme.textTheme.headlineSmall!.copyWith(
fontWeight: FontWeight.w600,
height: 32 / 24,
color: appTheme.black900,
);
static TextStyle get videoCallFlowPlaceholderConnectionLostBody => theme.textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.w400,
fontSize: 16,
height: 24 / 16,
letterSpacing: 0.5,
color: appTheme.black900,
);
static TextStyle get videoCallFlowPlaceholderConnectingSubtitle => theme.textTheme.bodyMedium!.copyWith(
fontSize: 13,
fontWeight: FontWeight.w400,
height: 24 / 13,
letterSpacing: 0.5,
color: appTheme.black900,
);
static TextStyle get videoCallFlowPlaceholderReconnectingSubtitle => theme.textTheme.bodyMedium!.copyWith(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 24 / 14,
color: appTheme.gray600,
);
static TextStyle get videoCallFlowPlaceholderConnectionLostDetail => theme.textTheme.bodyMedium!.copyWith(
fontSize: 13,
fontWeight: FontWeight.w400,
height: 1.35,
color: appTheme.red,
);
static TextStyle get videoCallFlowPlaceholderFilledButtonLabel => theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
color: appTheme.whiteA700,
);
} }
extension on TextStyle { extension on TextStyle {

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
/// Helper class for managing themes and colors. /// Helper class for managing themes and colors.
class ThemeHelper { class ThemeHelper {
@@ -34,7 +33,8 @@ class ThemeHelper {
var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme; var colorScheme = _supportedColorScheme[_appTheme] ?? ColorSchemes.primaryColorScheme;
return ThemeData( return ThemeData(
textTheme: GoogleFonts.mulishTextTheme(), fontFamily: 'Mulish-Local',
textTheme: ThemeData.light().textTheme.apply(fontFamily: 'Mulish-Local'),
visualDensity: VisualDensity.standard, visualDensity: VisualDensity.standard,
colorScheme: colorScheme, colorScheme: colorScheme,
scaffoldBackgroundColor: colorScheme.onPrimary, scaffoldBackgroundColor: colorScheme.onPrimary,
@@ -199,6 +199,12 @@ class PrimaryColors {
Color get grey7B => const Color(0xFF7B7B7B); Color get grey7B => const Color(0xFF7B7B7B);
Color get greyD9 => const Color(0xFFD9D9D9); Color get greyD9 => const Color(0xFFD9D9D9);
Color get videoCallControlOffBackground => const Color(0xFFED2F2F);
Color get videoCallPipIconBackground => const Color(0xFF565656);
Color get videoCallNoticeBannerBackground => const Color(0xFF9D9D9D);
} }
PrimaryColors get appTheme => ThemeHelper().themeColor(); PrimaryColors get appTheme => ThemeHelper().themeColor();

View File

@@ -1,9 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
abstract class TokenStorage { abstract class TokenStorage {
static SharedPreferences? _cachedPrefs;
static Future<SharedPreferences> _getPrefs() async {
_cachedPrefs ??= await SharedPreferences.getInstance();
return _cachedPrefs!;
}
/// Clears the cached SharedPreferences instance.
/// This is primarily used for testing to ensure fresh state between tests.
@visibleForTesting
static void clearCache() {
_cachedPrefs = null;
}
static Future<bool> haveTokenInStorage() async { static Future<bool> haveTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try { try {
final sharedPreferences = await _getPrefs();
if (sharedPreferences.getString('token') == null) { if (sharedPreferences.getString('token') == null) {
return false; return false;
} else { } else {
@@ -15,25 +30,25 @@ abstract class TokenStorage {
} }
static Future<String?> getTokenFromStorage() async { static Future<String?> getTokenFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await _getPrefs();
return sharedPreferences.getString('token'); return sharedPreferences.getString('token');
} }
static Future<void> saveTokenInStorage(String token) async { static Future<void> saveTokenInStorage(String token) async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await _getPrefs();
await sharedPreferences.setString('token', token); await sharedPreferences.setString('token', token);
} }
static Future<void> removeTokenInStorage() async { static Future<void> removeTokenInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await _getPrefs();
await sharedPreferences.remove('token'); await sharedPreferences.remove('token');
} }
} }
abstract class SocketIdStorage { abstract class SocketIdStorage {
static Future<bool> haveSocketIdInStorage() async { static Future<bool> haveSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance();
try { try {
final sharedPreferences = await TokenStorage._getPrefs();
if (sharedPreferences.getString('socketId') == null) { if (sharedPreferences.getString('socketId') == null) {
return false; return false;
} else { } else {
@@ -45,17 +60,17 @@ abstract class SocketIdStorage {
} }
static Future<String?> getSocketIdFromStorage() async { static Future<String?> getSocketIdFromStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await TokenStorage._getPrefs();
return sharedPreferences.getString('socketId'); return sharedPreferences.getString('socketId');
} }
static Future<void> saveSocketIdInStorage(String socketId) async { static Future<void> saveSocketIdInStorage(String socketId) async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await TokenStorage._getPrefs();
await sharedPreferences.setString('socketId', socketId); await sharedPreferences.setString('socketId', socketId);
} }
static Future<void> removeSocketIdInStorage() async { static Future<void> removeSocketIdInStorage() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await TokenStorage._getPrefs();
await sharedPreferences.remove('socketId'); await sharedPreferences.remove('socketId');
} }
} }

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