From e47f343afb5fac0cb8d8a5fe980a7ff76d332f9d Mon Sep 17 00:00:00 2001 From: Arseni Date: Tue, 11 Nov 2025 18:24:58 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BC=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 10244 bytes b2b/hi.dart | 1 + infra/.DS_Store | Bin 0 -> 8196 bytes 3 files changed, 1 insertion(+) create mode 100644 .DS_Store create mode 100644 b2b/hi.dart create mode 100644 infra/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..413e071a8796afef319a87d679f7dd7dd02e84e9 GIT binary patch literal 10244 zcmeHM%}*0S6n{ezwxY6NLDXnAsWBcfe24_%!BT3B9}z+ki=uV6UCM^-&SrPpsz}na z9{mIK?mytg#Ea3Z2R)e>ub%t^Jox6L6pMJ#7?hc0=J$5yy*GWoeY4w{1ptz+6h{CI z0MM{9)c0ZYlfuSX-BQ|QkB&&t9zx%8sfAZBEPso(>QD?Q1{4E|0mXn~U?(tu?`&4I zE`h353@8Q^0~-vm=YxTjVM=0MAb)gV!?ys4?byu=%4|EpYz&DhiFJWEf`tfEL}5zg z7J~?L>^G!dN@880Fb5(xA4JYfo zbZ#!c>XtqFNCZISLjd--1oXHWjZ>(~(+Kj>@|;hZip=aX&1B?x ztM=~m<7cnUH&dvxF3jeH literal 0 HcmV?d00001 diff --git a/b2b/hi.dart b/b2b/hi.dart new file mode 100644 index 0000000..2d9e1be --- /dev/null +++ b/b2b/hi.dart @@ -0,0 +1 @@ +// Hi \ No newline at end of file diff --git a/infra/.DS_Store b/infra/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e8acf6f25c351f86325870c33c03095bdc7decfc GIT binary patch literal 8196 zcmeHMPfrs;6o13tEr=|oAWAfw)Of;xKnTQxYrz=QSYi>2fOXprb;EY2+1*kQNqW|! zA3*PZ0WT&V{Q`P2@$ShF;K4V4Dp26y#VE{dW`1wqyf@SL+u1*F3IHTkEnWi{0ssRm zN3$E7BMR^5^-yV1nhr#Q{s2zC7FO~e%OA4U3eA9KKr^5j&{tX83oz07J%zIz< z>Q*zL890>;@aKbtm7}e&E>S!>un{Q$VjHSOK^gM^;bSSZ71kvRR~%Dj4@9{Vm0}R- zj{O!lN3<2zB}#W7(jADJnWzkfh}qH466Qc`iMrJcXa){5z;pL@D1i$R&;$AV`+UW7 z!)R>m3u$R>YwzeZI*o2)E%S`kGLaKi!;}-O@Uun6ynI7;@=GqQ<;=t^^CQRgX&@Zj zB1MzcN3LIFwG^xR#ZdTWWQP$mVmWhYXJ>q7YQ&nDp4=U=c4o%MMy$z+somX}F_0X+ z{vfyEm3{V720_$=0D78&dUY@&x3H;pB*@oBMBmK5Moh7;?lV1Sd(FOheM^ zoEtiSVR-mr&g{=Sg^g0cf9)*uSdj9nbfpxy#Z1L1xPB=c(XA-sKBqY|-emfK7N6{? z7Pl*Clh{*;;cf?%`eMBF`g%yC>^^xpXZD7LfO*~m3*E?N{_kKE~M9R|@ZWS)kAVfUmo?Ui=!0}5owY}y!dFrM86pJEO=9YZ40`nIF%16q&DD$wA zH-1QNllI-ZkdpRgsWBlb1^j$WG$e&2`LX;~$cw&Hrmlhou0Esd*wNsXw0)KS>CjyE%b z?=ioGTJF&WHDGuYp@OTt02|l_`}V5-=e2%hYN6{)~ZNHFv?0 literal 0 HcmV?d00001 -- 2.49.1 From ddb54ddfdc8d1175efc85ca89c2ce143dd7ff2a2 Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 13 Nov 2025 15:06:15 +0300 Subject: [PATCH 2/3] Frontend first draft --- .DS_Store | Bin 10244 -> 10244 bytes b2b/hi.dart | 1 - frontend/pshared/.gitignore | 16 + frontend/pshared/CHANGELOG.md | 3 + frontend/pshared/README.md | 39 + frontend/pshared/analysis_options.yaml | 30 + frontend/pshared/assets/flag_of_catalonia.si | Bin 0 -> 169 bytes frontend/pshared/l10n.yaml | 7 + .../lib/api/errors/authorization_failed.dart | 2 + .../lib/api/errors/failed_to_read_image.dart | 2 + .../pshared/lib/api/errors/unauthorized.dart | 2 + .../pshared/lib/api/errors/upload_failed.dart | 2 + .../lib/api/requests/change_password.dart | 18 + .../pshared/lib/api/requests/change_role.dart | 18 + .../pshared/lib/api/requests/file_upload.dart | 15 + frontend/pshared/lib/api/requests/login.dart | 24 + .../requests/permissions/change_policies.dart | 38 + frontend/pshared/lib/api/requests/signup.dart | 42 + .../api/requests/tokens/access_refresh.dart | 4 + .../api/requests/tokens/refresh_rotate.dart | 21 + .../lib/api/requests/tokens/session_id.dart | 18 + .../pshared/lib/api/responses/account.dart | 18 + frontend/pshared/lib/api/responses/base.dart | 16 + .../pshared/lib/api/responses/employees.dart | 18 + .../lib/api/responses/error/connectivity.dart | 13 + .../lib/api/responses/error/server.dart | 43 + .../lib/api/responses/file_uploaded.dart | 15 + frontend/pshared/lib/api/responses/login.dart | 19 + .../pshared/lib/api/responses/message.dart | 19 + .../lib/api/responses/organization.dart | 19 + .../pshared/lib/api/responses/policies.dart | 21 + frontend/pshared/lib/api/responses/token.dart | 15 + frontend/pshared/lib/api/responses/type.dart | 32 + frontend/pshared/lib/config/common.dart | 47 + frontend/pshared/lib/config/constants.dart | 2 + frontend/pshared/lib/config/mobile.dart | 35 + frontend/pshared/lib/config/web.dart | 75 + .../pshared/lib/data/dto/account/account.dart | 25 + .../pshared/lib/data/dto/account/base.dart | 27 + .../pshared/lib/data/dto/organization.dart | 23 + .../data/dto/organization/description.dart | 16 + .../lib/data/dto/permissions/access.dart | 22 + .../data/dto/permissions/action_effect.dart | 19 + .../data/dto/permissions/data/permission.dart | 28 + .../dto/permissions/data/permissions.dart | 24 + .../lib/data/dto/permissions/data/policy.dart | 26 + .../lib/data/dto/permissions/data/role.dart | 20 + .../permissions/description/description.dart | 21 + .../dto/permissions/description/policy.dart | 25 + .../dto/permissions/description/role.dart | 22 + .../pshared/lib/data/dto/pfe/services.dart | 0 frontend/pshared/lib/data/dto/storable.dart | 20 + frontend/pshared/lib/data/mapper/.DS_Store | Bin 0 -> 6148 bytes .../lib/data/mapper/account/account.dart | 26 + .../pshared/lib/data/mapper/account/base.dart | 24 + frontend/pshared/lib/data/mapper/icon.dart | 164 ++ .../pshared/lib/data/mapper/organization.dart | 22 + .../data/mapper/organization/description.dart | 15 + .../mapper/permissions/action_effect.dart | 23 + .../mapper/permissions/data/permission.dart | 33 + .../mapper/permissions/data/permissions.dart | 26 + .../data/mapper/permissions/data/policy.dart | 28 + .../data/mapper/permissions/data/role.dart | 25 + .../permissions/descriptions/description.dart | 23 + .../permissions/descriptions/policy.dart | 22 + .../mapper/permissions/descriptions/role.dart | 20 + .../pshared/lib/data/mapper/storable.dart | 11 + frontend/pshared/lib/l10n/ps_en.arb | 23 + .../pshared/lib/models/account/account.dart | 36 + frontend/pshared/lib/models/account/base.dart | 35 + .../lib/models/organization/description.dart | 7 + .../lib/models/organization/employee.dart | 4 + .../lib/models/organization/organization.dart | 34 + .../lib/models/payment/methods/card.dart | 18 + .../lib/models/payment/methods/data.dart | 6 + .../lib/models/payment/methods/iban.dart | 20 + .../models/payment/methods/russian_bank.dart | 26 + .../lib/models/payment/methods/type.dart | 21 + .../lib/models/payment/methods/wallet.dart | 12 + .../pshared/lib/models/payment/operation.dart | 30 + .../pshared/lib/models/payment/status.dart | 25 + frontend/pshared/lib/models/payment/type.dart | 6 + .../models/payment/upload_history_item.dart | 11 + .../pshared/lib/models/permission_bound.dart | 22 + .../lib/models/permission_bound_storable.dart | 6 + .../lib/models/permissions/access.dart | 13 + .../lib/models/permissions/action.dart | 15 + .../lib/models/permissions/action_effect.dart | 13 + .../models/permissions/data/permission.dart | 12 + .../models/permissions/data/permissions.dart | 16 + .../lib/models/permissions/data/policy.dart | 18 + .../lib/models/permissions/data/role.dart | 11 + .../permissions/descriptions/permissions.dart | 13 + .../permissions/descriptions/policy.dart | 22 + .../models/permissions/descriptions/role.dart | 27 + .../lib/models/permissions/effect.dart | 13 + frontend/pshared/lib/models/pfe/services.dart | 0 .../pshared/lib/models/recipient/filter.dart | 1 + .../lib/models/recipient/recipient.dart | 78 + .../pshared/lib/models/recipient/status.dart | 22 + .../pshared/lib/models/recipient/type.dart | 15 + frontend/pshared/lib/models/resources.dart | 107 ++ .../lib/models/settings/localizations.dart | 77 + .../lib/models/settings/time_validity.dart | 27 + frontend/pshared/lib/models/storable.dart | 30 + frontend/pshared/lib/provider/account.dart | 144 ++ .../lib/provider/accounts/employees.dart | 58 + frontend/pshared/lib/provider/exception.dart | 3 + frontend/pshared/lib/provider/locale.dart | 29 + .../pshared/lib/provider/organizations.dart | 80 + .../pshared/lib/provider/permissions.dart | 165 ++ .../pshared/lib/provider/pfe/provider.dart | 51 + frontend/pshared/lib/provider/resource.dart | 15 + frontend/pshared/lib/provider/services.dart | 0 frontend/pshared/lib/provider/template.dart | 170 ++ frontend/pshared/lib/pshared.dart | 8 + frontend/pshared/lib/service/account.dart | 61 + .../lib/service/accounts/employees.dart | 35 + .../lib/service/authorization/service.dart | 89 + .../lib/service/authorization/storage.dart | 53 + .../lib/service/authorization/token.dart | 85 + frontend/pshared/lib/service/device_id.dart | 24 + frontend/pshared/lib/service/files.dart | 38 + .../pshared/lib/service/organization.dart | 63 + frontend/pshared/lib/service/permissions.dart | 79 + frontend/pshared/lib/service/pfe/login.dart | 15 + frontend/pshared/lib/service/pfe/service.dart | 29 + .../pshared/lib/service/pfe/services.dart | 0 .../pshared/lib/service/secure_storage.dart | 25 + frontend/pshared/lib/service/services.dart | 32 + frontend/pshared/lib/service/template.dart | 66 + frontend/pshared/lib/utils/clipboard.dart | 11 + frontend/pshared/lib/utils/currency.dart | 22 + .../lib/utils/datetime_serializer.dart | 9 + .../pshared/lib/utils/flagged_locale.dart | 91 + frontend/pshared/lib/utils/http/client.dart | 2 + .../pshared/lib/utils/http/client/io.dart | 55 + .../pshared/lib/utils/http/client/stub.dart | 4 + .../pshared/lib/utils/http/client/web.dart | 9 + frontend/pshared/lib/utils/http/requests.dart | 165 ++ .../pshared/lib/utils/image/conversion.dart | 45 + .../pshared/lib/utils/image/transformed.dart | 6 + frontend/pshared/lib/utils/localization.dart | 136 ++ frontend/pshared/lib/utils/name_initials.dart | 17 + frontend/pshared/lib/utils/share.dart | 7 + frontend/pshared/lib/utils/snackbar.dart | 31 + frontend/pshared/lib/widgets/locale.dart | 65 + frontend/pshared/lib/widgets/template.dart | 28 + frontend/pshared/pubspec.yaml | 43 + frontend/pshared/test/test.dart | 6 + frontend/pweb/.gitignore | 45 + frontend/pweb/.metadata | 30 + frontend/pweb/README.md | 16 + frontend/pweb/analysis_options.yaml | 28 + frontend/pweb/android/.gitignore | 14 + frontend/pweb/android/app/build.gradle.kts | 44 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 + .../kotlin/com/example/web/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2588 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1768 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3534 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5255 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7074 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + frontend/pweb/android/build.gradle.kts | 21 + frontend/pweb/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + frontend/pweb/android/settings.gradle.kts | 25 + frontend/pweb/entrypoint.sh | 25 + frontend/pweb/ios/.gitignore | 34 + .../pweb/ios/Flutter/AppFrameworkInfo.plist | 26 + frontend/pweb/ios/Flutter/Debug.xcconfig | 2 + frontend/pweb/ios/Flutter/Release.xcconfig | 2 + frontend/pweb/ios/Podfile | 43 + frontend/pweb/ios/Podfile.lock | 61 + .../pweb/ios/Runner.xcodeproj/project.pbxproj | 731 ++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + frontend/pweb/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 1 + .../Icon-App-1024x1024@1x.png | Bin 0 -> 40307 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 733 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1409 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 2138 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1046 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 2076 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 3182 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1409 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 2922 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 4392 bytes .../AppIcon.appiconset/Icon-App-50x50@1x.png | Bin 0 -> 1811 bytes .../AppIcon.appiconset/Icon-App-50x50@2x.png | Bin 0 -> 3577 bytes .../AppIcon.appiconset/Icon-App-57x57@1x.png | Bin 0 -> 2076 bytes .../AppIcon.appiconset/Icon-App-57x57@2x.png | Bin 0 -> 4198 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 4392 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 6630 bytes .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 0 -> 2588 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 0 -> 5255 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 2785 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 5622 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 6190 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + frontend/pweb/ios/Runner/Info.plist | 49 + .../pweb/ios/Runner/Runner-Bridging-Header.h | 1 + .../pweb/ios/RunnerTests/RunnerTests.swift | 12 + frontend/pweb/l10n.yaml | 5 + frontend/pweb/lib/app/app.dart | 34 + frontend/pweb/lib/app/locale_manager.dart | 70 + frontend/pweb/lib/app/router/page_params.dart | 15 + frontend/pweb/lib/app/router/pages.dart | 60 + frontend/pweb/lib/app/router/router.dart | 57 + frontend/pweb/lib/app/timeago.dart | 27 + frontend/pweb/lib/config/constants.dart | 10 + .../lib/generated/i18n/app_localizations.dart | 1562 +++++++++++++++++ .../generated/i18n/app_localizations_en.dart | 779 ++++++++ .../generated/i18n/app_localizations_ru.dart | 782 +++++++++ frontend/pweb/lib/l10n/en.arb | 435 +++++ frontend/pweb/lib/l10n/ru.arb | 426 +++++ frontend/pweb/lib/main.dart | 104 ++ frontend/pweb/lib/models/currency.dart | 1 + frontend/pweb/lib/models/wallet.dart | 38 + .../pweb/lib/pages/2fa/error_message.dart | 16 + frontend/pweb/lib/pages/2fa/input.dart | 35 + frontend/pweb/lib/pages/2fa/page.dart | 61 + frontend/pweb/lib/pages/2fa/prompt.dart | 15 + frontend/pweb/lib/pages/2fa/resend.dart | 31 + .../pages/address_book/form/method_tile.dart | 90 + .../lib/pages/address_book/form/page.dart | 109 ++ .../lib/pages/address_book/form/view.dart | 124 ++ .../address_book/form/widgets/button.dart | 56 + .../form/widgets/choice_chips.dart | 62 + .../form/widgets/email_field.dart | 36 + .../address_book/form/widgets/header.dart | 44 + .../address_book/form/widgets/name_field.dart | 35 + .../address_book/page/filter_button.dart | 61 + .../lib/pages/address_book/page/header.dart | 44 + .../lib/pages/address_book/page/list.dart | 40 + .../lib/pages/address_book/page/page.dart | 102 ++ .../address_book/page/recipient/actions.dart | 19 + .../page/recipient/info_column.dart | 20 + .../address_book/page/recipient/info_row.dart | 58 + .../address_book/page/recipient/item.dart | 97 + .../page/recipient/payment_row.dart | 47 + .../address_book/page/recipient/status.dart | 32 + .../lib/pages/address_book/page/search.dart | 44 + .../dashboard/buttons/balance/add_funds.dart | 52 + .../dashboard/buttons/balance/amount.dart | 46 + .../dashboard/buttons/balance/balance.dart | 32 + .../pages/dashboard/buttons/balance/card.dart | 54 + .../dashboard/buttons/balance/carousel.dart | 113 ++ .../dashboard/buttons/balance/config.dart | 15 + .../dashboard/buttons/balance/header.dart | 39 + .../dashboard/buttons/balance/indicator.dart | 50 + .../lib/pages/dashboard/buttons/buttons.dart | 68 + .../pweb/lib/pages/dashboard/dashboard.dart | 92 + .../pages/dashboard/organization/button.dart | 12 + .../pages/dashboard/payouts/multiple/csv.dart | 70 + .../dashboard/payouts/multiple/form.dart | 13 + .../dashboard/payouts/multiple/history.dart | 69 + .../dashboard/payouts/multiple/sample.dart | 71 + .../dashboard/payouts/multiple/title.dart | 30 + .../dashboard/payouts/multiple/widget.dart | 57 + .../pages/dashboard/payouts/payment_form.dart | 92 + .../payouts/single/adress_book/avatar.dart | 54 + .../adress_book/long_list/info_row.dart | 23 + .../single/adress_book/long_list/item.dart | 83 + .../adress_book/long_list/long_list.dart | 31 + .../single/adress_book/short_list.dart | 51 + .../payouts/single/adress_book/widget.dart | 103 ++ .../payouts/single/form/details.dart | 69 + .../dashboard/payouts/single/form/header.dart | 32 + .../payouts/single/new_recipient/payout.dart | 52 + .../payouts/single/new_recipient/type.dart | 41 + .../dashboard/payouts/single/widget.dart | 35 + frontend/pweb/lib/pages/errors/error.dart | 61 + frontend/pweb/lib/pages/errors/not_found.dart | 19 + frontend/pweb/lib/pages/loader.dart | 19 + frontend/pweb/lib/pages/loaders/account.dart | 38 + .../pweb/lib/pages/loaders/organization.dart | 37 + .../pweb/lib/pages/loaders/permissions.dart | 37 + frontend/pweb/lib/pages/login/app_bar.dart | 21 + frontend/pweb/lib/pages/login/buttons.dart | 28 + frontend/pweb/lib/pages/login/form.dart | 106 ++ frontend/pweb/lib/pages/login/header.dart | 24 + frontend/pweb/lib/pages/login/login.dart | 40 + frontend/pweb/lib/pages/login/page.dart | 16 + frontend/pweb/lib/pages/login/signup.dart | 21 + .../lib/pages/payment_methods/add/card.dart | 112 ++ .../lib/pages/payment_methods/add/iban.dart | 122 ++ .../payment_methods/add/method_selector.dart | 34 + .../payment_methods/add/russian_bank.dart | 162 ++ .../lib/pages/payment_methods/add/wallet.dart | 62 + .../lib/pages/payment_methods/add/widget.dart | 76 + .../payment_methods/delete_confirmation.dart | 23 + .../pweb/lib/pages/payment_methods/form.dart | 55 + .../pweb/lib/pages/payment_methods/icon.dart | 17 + .../pweb/lib/pages/payment_methods/page.dart | 232 +++ .../pweb/lib/pages/payment_methods/title.dart | 100 ++ .../pages/payment_page/methods/advanced.dart | 20 + .../payment_page/methods/controller.dart | 71 + .../pages/payment_page/methods/header.dart | 36 + .../lib/pages/payment_page/methods/list.dart | 40 + .../pages/payment_page/methods/widget.dart | 50 + .../pweb/lib/pages/payment_page/page.dart | 37 + .../lib/pages/payment_page/wallet/card.dart | 61 + .../wallet/edit/buttons/buttons.dart | 51 + .../wallet/edit/buttons/save.dart | 52 + .../wallet/edit/buttons/send.dart | 29 + .../wallet/edit/buttons/top_up.dart | 22 + .../payment_page/wallet/edit/fields.dart | 50 + .../payment_page/wallet/edit/header.dart | 122 ++ .../pages/payment_page/wallet/edit/page.dart | 55 + .../lib/pages/payment_page/wallet/wigets.dart | 49 + .../lib/pages/report/charts/distribution.dart | 103 ++ .../pweb/lib/pages/report/charts/status.dart | 91 + frontend/pweb/lib/pages/report/page.dart | 170 ++ .../pweb/lib/pages/report/table/badge.dart | 55 + .../pweb/lib/pages/report/table/filters.dart | 125 ++ frontend/pweb/lib/pages/report/table/row.dart | 23 + .../pweb/lib/pages/report/table/widget.dart | 63 + .../settings/profile/account/avatar.dart | 107 ++ .../settings/profile/account/locale.dart | 86 + .../pages/settings/profile/account/name.dart | 127 ++ .../pweb/lib/pages/settings/profile/page.dart | 53 + .../pweb/lib/pages/settings/widgets/base.dart | 143 ++ .../lib/pages/settings/widgets/image.dart | 112 ++ .../pweb/lib/pages/settings/widgets/pick.dart | 114 ++ .../pweb/lib/pages/settings/widgets/text.dart | 88 + frontend/pweb/lib/pages/signup/buttons.dart | 31 + .../pweb/lib/pages/signup/form/buttons.dart | 16 + .../pweb/lib/pages/signup/form/content.dart | 62 + .../lib/pages/signup/form/controllers.dart | 22 + .../lib/pages/signup/form/description.dart | 24 + .../pweb/lib/pages/signup/form/email.dart | 32 + .../pweb/lib/pages/signup/form/feilds.dart | 70 + frontend/pweb/lib/pages/signup/form/form.dart | 11 + .../pweb/lib/pages/signup/form/state.dart | 94 + frontend/pweb/lib/pages/signup/header.dart | 24 + frontend/pweb/lib/pages/signup/page.dart | 16 + frontend/pweb/lib/pages/with_footer.dart | 22 + frontend/pweb/lib/providers/balance.dart | 32 + frontend/pweb/lib/providers/carousel.dart | 15 + frontend/pweb/lib/providers/mock_payment.dart | 24 + .../pweb/lib/providers/page_selector.dart | 99 ++ .../pweb/lib/providers/payment_methods.dart | 66 + frontend/pweb/lib/providers/recipient.dart | 80 + frontend/pweb/lib/providers/template.dart | 31 + frontend/pweb/lib/providers/two_factor.dart | 38 + .../pweb/lib/providers/upload_history.dart | 10 + frontend/pweb/lib/providers/wallets.dart | 126 ++ frontend/pweb/lib/services/amplitude.dart | 210 +++ frontend/pweb/lib/services/auth.dart | 12 + frontend/pweb/lib/services/balance.dart | 22 + .../services/payments/payment_methods.dart | 42 + .../lib/services/payments/upload_history.dart | 20 + .../lib/services/recipient/recipient.dart | 88 + frontend/pweb/lib/services/wallets.dart | 41 + frontend/pweb/lib/utils/clipboard.dart | 11 + frontend/pweb/lib/utils/currency.dart | 38 + frontend/pweb/lib/utils/dimensions.dart | 48 + frontend/pweb/lib/utils/error/content.dart | 27 + frontend/pweb/lib/utils/error/handler.dart | 66 + frontend/pweb/lib/utils/error/snackbar.dart | 132 ++ frontend/pweb/lib/utils/error_handler.dart | 66 + frontend/pweb/lib/utils/flagged_locale.dart | 55 + frontend/pweb/lib/utils/http.dart | 12 + frontend/pweb/lib/utils/initials.dart | 5 + frontend/pweb/lib/utils/notify.dart | 38 + frontend/pweb/lib/utils/payment/dropdown.dart | 64 + frontend/pweb/lib/utils/payment/label.dart | 16 + .../pweb/lib/utils/payment/selector_type.dart | 54 + frontend/pweb/lib/utils/share.dart | 40 + frontend/pweb/lib/utils/snackbar.dart | 29 + .../lib/utils/snapshot_haserror_check.dart | 12 + .../pweb/lib/utils/text_field_styles.dart | 19 + frontend/pweb/lib/utils/time_ago.dart | 20 + frontend/pweb/lib/widgets/appbar/app_bar.dart | 48 + .../lib/widgets/appbar/notifications.dart | 13 + frontend/pweb/lib/widgets/appbar/profile.dart | 40 + .../pweb/lib/widgets/constrained_form.dart | 35 + frontend/pweb/lib/widgets/drawer/avatar.dart | 30 + .../lib/widgets/drawer/tiles/dashboard.dart | 19 + .../pweb/lib/widgets/drawer/tiles/logout.dart | 32 + .../drawer/tiles/settings/permissions.dart | 18 + .../drawer/tiles/settings/profile.dart | 19 + .../widgets/drawer/tiles/settings/roles.dart | 19 + .../widgets/drawer/tiles/settings/users.dart | 21 + frontend/pweb/lib/widgets/drawer/widget.dart | 45 + .../lib/widgets/employee/avatar/provider.dart | 26 + .../lib/widgets/employee/avatar/widget.dart | 29 + .../pweb/lib/widgets/employee/provider.dart | 26 + frontend/pweb/lib/widgets/employee/tile.dart | 34 + frontend/pweb/lib/widgets/error/content.dart | 29 + frontend/pweb/lib/widgets/error/snackbar.dart | 132 ++ frontend/pweb/lib/widgets/footer/labels.dart | 24 + .../pweb/lib/widgets/footer/policies.dart | 58 + frontend/pweb/lib/widgets/footer/support.dart | 49 + frontend/pweb/lib/widgets/footer/widget.dart | 28 + frontend/pweb/lib/widgets/hspacer.dart | 14 + frontend/pweb/lib/widgets/logo.dart | 15 + .../pweb/lib/widgets/password/hint/error.dart | 23 + .../pweb/lib/widgets/password/hint/full.dart | 18 + .../pweb/lib/widgets/password/hint/short.dart | 25 + .../password/hint/validation_result.dart | 36 + .../lib/widgets/password/hint/widget.dart | 23 + .../pweb/lib/widgets/password/password.dart | 104 ++ .../pweb/lib/widgets/password/verify.dart | 95 + .../pweb/lib/widgets/protected/widget.dart | 16 + frontend/pweb/lib/widgets/search.dart | 52 + .../lib/widgets/sidebar/destinations.dart | 46 + frontend/pweb/lib/widgets/sidebar/page.dart | 111 ++ .../pweb/lib/widgets/sidebar/side_menu.dart | 79 + .../pweb/lib/widgets/sidebar/sidebar.dart | 59 + frontend/pweb/lib/widgets/sidebar/user.dart | 70 + frontend/pweb/lib/widgets/stats/card.dart | 47 + frontend/pweb/lib/widgets/text_field.dart | 38 + frontend/pweb/lib/widgets/username.dart | 43 + frontend/pweb/lib/widgets/vspacer.dart | 14 + frontend/pweb/linux/.gitignore | 1 + frontend/pweb/linux/CMakeLists.txt | 128 ++ frontend/pweb/linux/flutter/CMakeLists.txt | 88 + .../flutter/generated_plugin_registrant.cc | 23 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 26 + frontend/pweb/linux/runner/CMakeLists.txt | 26 + frontend/pweb/linux/runner/main.cc | 6 + frontend/pweb/linux/runner/my_application.cc | 130 ++ frontend/pweb/linux/runner/my_application.h | 18 + frontend/pweb/macos/.gitignore | 7 + .../pweb/macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 26 + frontend/pweb/macos/Podfile | 42 + .../macos/Runner.xcodeproj/project.pbxproj | 705 ++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + frontend/pweb/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 + .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../pweb/macos/Runner/Base.lproj/MainMenu.xib | 343 ++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../pweb/macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + frontend/pweb/macos/Runner/Info.plist | 32 + .../pweb/macos/Runner/MainFlutterWindow.swift | 15 + .../pweb/macos/Runner/Release.entitlements | 8 + .../pweb/macos/RunnerTests/RunnerTests.swift | 12 + frontend/pweb/pubspec.lock | 1374 +++++++++++++++ frontend/pweb/pubspec.yaml | 140 ++ frontend/pweb/resources/logo.png | Bin 0 -> 34400 bytes frontend/pweb/resources/logo.si | Bin 0 -> 5664 bytes frontend/pweb/test/widget_test.dart | 30 + frontend/pweb/untranslated.txt | 11 + frontend/pweb/web/favicon.png | Bin 0 -> 560 bytes frontend/pweb/web/icons/Icon-192.png | Bin 0 -> 7074 bytes frontend/pweb/web/icons/Icon-512.png | Bin 0 -> 19551 bytes frontend/pweb/web/icons/Icon-maskable-192.png | Bin 0 -> 7074 bytes frontend/pweb/web/icons/Icon-maskable-512.png | Bin 0 -> 19551 bytes frontend/pweb/web/index.html | 38 + frontend/pweb/web/manifest.json | 35 + frontend/pweb/windows/.gitignore | 17 + frontend/pweb/windows/CMakeLists.txt | 108 ++ frontend/pweb/windows/flutter/CMakeLists.txt | 109 ++ .../flutter/generated_plugin_registrant.cc | 23 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 27 + frontend/pweb/windows/runner/CMakeLists.txt | 40 + frontend/pweb/windows/runner/Runner.rc | 121 ++ .../pweb/windows/runner/flutter_window.cpp | 71 + frontend/pweb/windows/runner/flutter_window.h | 33 + frontend/pweb/windows/runner/main.cpp | 43 + frontend/pweb/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../pweb/windows/runner/runner.exe.manifest | 14 + frontend/pweb/windows/runner/utils.cpp | 65 + frontend/pweb/windows/runner/utils.h | 19 + frontend/pweb/windows/runner/win32_window.cpp | 288 +++ frontend/pweb/windows/runner/win32_window.h | 102 ++ 504 files changed, 25498 insertions(+), 1 deletion(-) delete mode 100644 b2b/hi.dart create mode 100644 frontend/pshared/.gitignore create mode 100644 frontend/pshared/CHANGELOG.md create mode 100644 frontend/pshared/README.md create mode 100644 frontend/pshared/analysis_options.yaml create mode 100644 frontend/pshared/assets/flag_of_catalonia.si create mode 100644 frontend/pshared/l10n.yaml create mode 100644 frontend/pshared/lib/api/errors/authorization_failed.dart create mode 100644 frontend/pshared/lib/api/errors/failed_to_read_image.dart create mode 100644 frontend/pshared/lib/api/errors/unauthorized.dart create mode 100644 frontend/pshared/lib/api/errors/upload_failed.dart create mode 100644 frontend/pshared/lib/api/requests/change_password.dart create mode 100644 frontend/pshared/lib/api/requests/change_role.dart create mode 100644 frontend/pshared/lib/api/requests/file_upload.dart create mode 100644 frontend/pshared/lib/api/requests/login.dart create mode 100644 frontend/pshared/lib/api/requests/permissions/change_policies.dart create mode 100644 frontend/pshared/lib/api/requests/signup.dart create mode 100644 frontend/pshared/lib/api/requests/tokens/access_refresh.dart create mode 100644 frontend/pshared/lib/api/requests/tokens/refresh_rotate.dart create mode 100644 frontend/pshared/lib/api/requests/tokens/session_id.dart create mode 100644 frontend/pshared/lib/api/responses/account.dart create mode 100644 frontend/pshared/lib/api/responses/base.dart create mode 100644 frontend/pshared/lib/api/responses/employees.dart create mode 100644 frontend/pshared/lib/api/responses/error/connectivity.dart create mode 100644 frontend/pshared/lib/api/responses/error/server.dart create mode 100644 frontend/pshared/lib/api/responses/file_uploaded.dart create mode 100644 frontend/pshared/lib/api/responses/login.dart create mode 100644 frontend/pshared/lib/api/responses/message.dart create mode 100644 frontend/pshared/lib/api/responses/organization.dart create mode 100644 frontend/pshared/lib/api/responses/policies.dart create mode 100644 frontend/pshared/lib/api/responses/token.dart create mode 100644 frontend/pshared/lib/api/responses/type.dart create mode 100644 frontend/pshared/lib/config/common.dart create mode 100644 frontend/pshared/lib/config/constants.dart create mode 100644 frontend/pshared/lib/config/mobile.dart create mode 100644 frontend/pshared/lib/config/web.dart create mode 100644 frontend/pshared/lib/data/dto/account/account.dart create mode 100644 frontend/pshared/lib/data/dto/account/base.dart create mode 100644 frontend/pshared/lib/data/dto/organization.dart create mode 100644 frontend/pshared/lib/data/dto/organization/description.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/access.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/action_effect.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/data/permission.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/data/permissions.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/data/policy.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/data/role.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/description/description.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/description/policy.dart create mode 100644 frontend/pshared/lib/data/dto/permissions/description/role.dart create mode 100644 frontend/pshared/lib/data/dto/pfe/services.dart create mode 100644 frontend/pshared/lib/data/dto/storable.dart create mode 100644 frontend/pshared/lib/data/mapper/.DS_Store create mode 100644 frontend/pshared/lib/data/mapper/account/account.dart create mode 100644 frontend/pshared/lib/data/mapper/account/base.dart create mode 100644 frontend/pshared/lib/data/mapper/icon.dart create mode 100644 frontend/pshared/lib/data/mapper/organization.dart create mode 100644 frontend/pshared/lib/data/mapper/organization/description.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/action_effect.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/data/permission.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/data/permissions.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/data/policy.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/data/role.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/descriptions/description.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart create mode 100644 frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart create mode 100644 frontend/pshared/lib/data/mapper/storable.dart create mode 100644 frontend/pshared/lib/l10n/ps_en.arb create mode 100644 frontend/pshared/lib/models/account/account.dart create mode 100644 frontend/pshared/lib/models/account/base.dart create mode 100644 frontend/pshared/lib/models/organization/description.dart create mode 100644 frontend/pshared/lib/models/organization/employee.dart create mode 100644 frontend/pshared/lib/models/organization/organization.dart create mode 100644 frontend/pshared/lib/models/payment/methods/card.dart create mode 100644 frontend/pshared/lib/models/payment/methods/data.dart create mode 100644 frontend/pshared/lib/models/payment/methods/iban.dart create mode 100644 frontend/pshared/lib/models/payment/methods/russian_bank.dart create mode 100644 frontend/pshared/lib/models/payment/methods/type.dart create mode 100644 frontend/pshared/lib/models/payment/methods/wallet.dart create mode 100644 frontend/pshared/lib/models/payment/operation.dart create mode 100644 frontend/pshared/lib/models/payment/status.dart create mode 100644 frontend/pshared/lib/models/payment/type.dart create mode 100644 frontend/pshared/lib/models/payment/upload_history_item.dart create mode 100644 frontend/pshared/lib/models/permission_bound.dart create mode 100644 frontend/pshared/lib/models/permission_bound_storable.dart create mode 100644 frontend/pshared/lib/models/permissions/access.dart create mode 100644 frontend/pshared/lib/models/permissions/action.dart create mode 100644 frontend/pshared/lib/models/permissions/action_effect.dart create mode 100644 frontend/pshared/lib/models/permissions/data/permission.dart create mode 100644 frontend/pshared/lib/models/permissions/data/permissions.dart create mode 100644 frontend/pshared/lib/models/permissions/data/policy.dart create mode 100644 frontend/pshared/lib/models/permissions/data/role.dart create mode 100644 frontend/pshared/lib/models/permissions/descriptions/permissions.dart create mode 100644 frontend/pshared/lib/models/permissions/descriptions/policy.dart create mode 100644 frontend/pshared/lib/models/permissions/descriptions/role.dart create mode 100644 frontend/pshared/lib/models/permissions/effect.dart create mode 100644 frontend/pshared/lib/models/pfe/services.dart create mode 100644 frontend/pshared/lib/models/recipient/filter.dart create mode 100644 frontend/pshared/lib/models/recipient/recipient.dart create mode 100644 frontend/pshared/lib/models/recipient/status.dart create mode 100644 frontend/pshared/lib/models/recipient/type.dart create mode 100644 frontend/pshared/lib/models/resources.dart create mode 100644 frontend/pshared/lib/models/settings/localizations.dart create mode 100644 frontend/pshared/lib/models/settings/time_validity.dart create mode 100644 frontend/pshared/lib/models/storable.dart create mode 100644 frontend/pshared/lib/provider/account.dart create mode 100644 frontend/pshared/lib/provider/accounts/employees.dart create mode 100644 frontend/pshared/lib/provider/exception.dart create mode 100644 frontend/pshared/lib/provider/locale.dart create mode 100644 frontend/pshared/lib/provider/organizations.dart create mode 100644 frontend/pshared/lib/provider/permissions.dart create mode 100644 frontend/pshared/lib/provider/pfe/provider.dart create mode 100644 frontend/pshared/lib/provider/resource.dart create mode 100644 frontend/pshared/lib/provider/services.dart create mode 100644 frontend/pshared/lib/provider/template.dart create mode 100644 frontend/pshared/lib/pshared.dart create mode 100644 frontend/pshared/lib/service/account.dart create mode 100644 frontend/pshared/lib/service/accounts/employees.dart create mode 100644 frontend/pshared/lib/service/authorization/service.dart create mode 100644 frontend/pshared/lib/service/authorization/storage.dart create mode 100644 frontend/pshared/lib/service/authorization/token.dart create mode 100644 frontend/pshared/lib/service/device_id.dart create mode 100644 frontend/pshared/lib/service/files.dart create mode 100644 frontend/pshared/lib/service/organization.dart create mode 100644 frontend/pshared/lib/service/permissions.dart create mode 100644 frontend/pshared/lib/service/pfe/login.dart create mode 100644 frontend/pshared/lib/service/pfe/service.dart create mode 100644 frontend/pshared/lib/service/pfe/services.dart create mode 100644 frontend/pshared/lib/service/secure_storage.dart create mode 100644 frontend/pshared/lib/service/services.dart create mode 100644 frontend/pshared/lib/service/template.dart create mode 100644 frontend/pshared/lib/utils/clipboard.dart create mode 100644 frontend/pshared/lib/utils/currency.dart create mode 100644 frontend/pshared/lib/utils/datetime_serializer.dart create mode 100644 frontend/pshared/lib/utils/flagged_locale.dart create mode 100644 frontend/pshared/lib/utils/http/client.dart create mode 100644 frontend/pshared/lib/utils/http/client/io.dart create mode 100644 frontend/pshared/lib/utils/http/client/stub.dart create mode 100644 frontend/pshared/lib/utils/http/client/web.dart create mode 100644 frontend/pshared/lib/utils/http/requests.dart create mode 100644 frontend/pshared/lib/utils/image/conversion.dart create mode 100644 frontend/pshared/lib/utils/image/transformed.dart create mode 100644 frontend/pshared/lib/utils/localization.dart create mode 100644 frontend/pshared/lib/utils/name_initials.dart create mode 100644 frontend/pshared/lib/utils/share.dart create mode 100644 frontend/pshared/lib/utils/snackbar.dart create mode 100644 frontend/pshared/lib/widgets/locale.dart create mode 100644 frontend/pshared/lib/widgets/template.dart create mode 100644 frontend/pshared/pubspec.yaml create mode 100644 frontend/pshared/test/test.dart create mode 100644 frontend/pweb/.gitignore create mode 100644 frontend/pweb/.metadata create mode 100644 frontend/pweb/README.md create mode 100644 frontend/pweb/analysis_options.yaml create mode 100644 frontend/pweb/android/.gitignore create mode 100644 frontend/pweb/android/app/build.gradle.kts create mode 100644 frontend/pweb/android/app/src/debug/AndroidManifest.xml create mode 100644 frontend/pweb/android/app/src/main/AndroidManifest.xml create mode 100644 frontend/pweb/android/app/src/main/kotlin/com/example/web/MainActivity.kt create mode 100644 frontend/pweb/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 frontend/pweb/android/app/src/main/res/drawable/launch_background.xml create mode 100644 frontend/pweb/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 frontend/pweb/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 frontend/pweb/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 frontend/pweb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 frontend/pweb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 frontend/pweb/android/app/src/main/res/values-night/styles.xml create mode 100644 frontend/pweb/android/app/src/main/res/values/styles.xml create mode 100644 frontend/pweb/android/app/src/profile/AndroidManifest.xml create mode 100644 frontend/pweb/android/build.gradle.kts create mode 100644 frontend/pweb/android/gradle.properties create mode 100644 frontend/pweb/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 frontend/pweb/android/settings.gradle.kts create mode 100755 frontend/pweb/entrypoint.sh create mode 100644 frontend/pweb/ios/.gitignore create mode 100644 frontend/pweb/ios/Flutter/AppFrameworkInfo.plist create mode 100644 frontend/pweb/ios/Flutter/Debug.xcconfig create mode 100644 frontend/pweb/ios/Flutter/Release.xcconfig create mode 100644 frontend/pweb/ios/Podfile create mode 100644 frontend/pweb/ios/Podfile.lock create mode 100644 frontend/pweb/ios/Runner.xcodeproj/project.pbxproj create mode 100644 frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 frontend/pweb/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 frontend/pweb/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/pweb/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/pweb/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 frontend/pweb/ios/Runner/AppDelegate.swift create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 frontend/pweb/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 frontend/pweb/ios/Runner/Base.lproj/Main.storyboard create mode 100644 frontend/pweb/ios/Runner/Info.plist create mode 100644 frontend/pweb/ios/Runner/Runner-Bridging-Header.h create mode 100644 frontend/pweb/ios/RunnerTests/RunnerTests.swift create mode 100644 frontend/pweb/l10n.yaml create mode 100644 frontend/pweb/lib/app/app.dart create mode 100644 frontend/pweb/lib/app/locale_manager.dart create mode 100644 frontend/pweb/lib/app/router/page_params.dart create mode 100644 frontend/pweb/lib/app/router/pages.dart create mode 100644 frontend/pweb/lib/app/router/router.dart create mode 100644 frontend/pweb/lib/app/timeago.dart create mode 100644 frontend/pweb/lib/config/constants.dart create mode 100644 frontend/pweb/lib/generated/i18n/app_localizations.dart create mode 100644 frontend/pweb/lib/generated/i18n/app_localizations_en.dart create mode 100644 frontend/pweb/lib/generated/i18n/app_localizations_ru.dart create mode 100644 frontend/pweb/lib/l10n/en.arb create mode 100644 frontend/pweb/lib/l10n/ru.arb create mode 100644 frontend/pweb/lib/main.dart create mode 100644 frontend/pweb/lib/models/currency.dart create mode 100644 frontend/pweb/lib/models/wallet.dart create mode 100644 frontend/pweb/lib/pages/2fa/error_message.dart create mode 100644 frontend/pweb/lib/pages/2fa/input.dart create mode 100644 frontend/pweb/lib/pages/2fa/page.dart create mode 100644 frontend/pweb/lib/pages/2fa/prompt.dart create mode 100644 frontend/pweb/lib/pages/2fa/resend.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/method_tile.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/page.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/view.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/button.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/choice_chips.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/email_field.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/header.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/name_field.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/filter_button.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/header.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/list.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/page.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/recipient/actions.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/recipient/info_column.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/recipient/info_row.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/recipient/item.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/recipient/payment_row.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/recipient/status.dart create mode 100644 frontend/pweb/lib/pages/address_book/page/search.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/indicator.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/buttons.dart create mode 100644 frontend/pweb/lib/pages/dashboard/dashboard.dart create mode 100644 frontend/pweb/lib/pages/dashboard/organization/button.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/multiple/csv.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/multiple/form.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/multiple/sample.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/multiple/title.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/multiple/widget.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/payment_form.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/avatar.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/short_list.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/form/details.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/form/header.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/payout.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/type.dart create mode 100644 frontend/pweb/lib/pages/dashboard/payouts/single/widget.dart create mode 100644 frontend/pweb/lib/pages/errors/error.dart create mode 100644 frontend/pweb/lib/pages/errors/not_found.dart create mode 100644 frontend/pweb/lib/pages/loader.dart create mode 100644 frontend/pweb/lib/pages/loaders/account.dart create mode 100644 frontend/pweb/lib/pages/loaders/organization.dart create mode 100644 frontend/pweb/lib/pages/loaders/permissions.dart create mode 100644 frontend/pweb/lib/pages/login/app_bar.dart create mode 100644 frontend/pweb/lib/pages/login/buttons.dart create mode 100644 frontend/pweb/lib/pages/login/form.dart create mode 100644 frontend/pweb/lib/pages/login/header.dart create mode 100644 frontend/pweb/lib/pages/login/login.dart create mode 100644 frontend/pweb/lib/pages/login/page.dart create mode 100644 frontend/pweb/lib/pages/login/signup.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/add/card.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/add/iban.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/add/method_selector.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/add/wallet.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/add/widget.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/form.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/icon.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/page.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/title.dart create mode 100644 frontend/pweb/lib/pages/payment_page/methods/advanced.dart create mode 100644 frontend/pweb/lib/pages/payment_page/methods/controller.dart create mode 100644 frontend/pweb/lib/pages/payment_page/methods/header.dart create mode 100644 frontend/pweb/lib/pages/payment_page/methods/list.dart create mode 100644 frontend/pweb/lib/pages/payment_page/methods/widget.dart create mode 100644 frontend/pweb/lib/pages/payment_page/page.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/card.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/buttons.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/save.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/top_up.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart create mode 100644 frontend/pweb/lib/pages/payment_page/wallet/wigets.dart create mode 100644 frontend/pweb/lib/pages/report/charts/distribution.dart create mode 100644 frontend/pweb/lib/pages/report/charts/status.dart create mode 100644 frontend/pweb/lib/pages/report/page.dart create mode 100644 frontend/pweb/lib/pages/report/table/badge.dart create mode 100644 frontend/pweb/lib/pages/report/table/filters.dart create mode 100644 frontend/pweb/lib/pages/report/table/row.dart create mode 100644 frontend/pweb/lib/pages/report/table/widget.dart create mode 100644 frontend/pweb/lib/pages/settings/profile/account/avatar.dart create mode 100644 frontend/pweb/lib/pages/settings/profile/account/locale.dart create mode 100644 frontend/pweb/lib/pages/settings/profile/account/name.dart create mode 100644 frontend/pweb/lib/pages/settings/profile/page.dart create mode 100644 frontend/pweb/lib/pages/settings/widgets/base.dart create mode 100644 frontend/pweb/lib/pages/settings/widgets/image.dart create mode 100644 frontend/pweb/lib/pages/settings/widgets/pick.dart create mode 100644 frontend/pweb/lib/pages/settings/widgets/text.dart create mode 100644 frontend/pweb/lib/pages/signup/buttons.dart create mode 100644 frontend/pweb/lib/pages/signup/form/buttons.dart create mode 100644 frontend/pweb/lib/pages/signup/form/content.dart create mode 100644 frontend/pweb/lib/pages/signup/form/controllers.dart create mode 100644 frontend/pweb/lib/pages/signup/form/description.dart create mode 100644 frontend/pweb/lib/pages/signup/form/email.dart create mode 100644 frontend/pweb/lib/pages/signup/form/feilds.dart create mode 100644 frontend/pweb/lib/pages/signup/form/form.dart create mode 100644 frontend/pweb/lib/pages/signup/form/state.dart create mode 100644 frontend/pweb/lib/pages/signup/header.dart create mode 100644 frontend/pweb/lib/pages/signup/page.dart create mode 100644 frontend/pweb/lib/pages/with_footer.dart create mode 100644 frontend/pweb/lib/providers/balance.dart create mode 100644 frontend/pweb/lib/providers/carousel.dart create mode 100644 frontend/pweb/lib/providers/mock_payment.dart create mode 100644 frontend/pweb/lib/providers/page_selector.dart create mode 100644 frontend/pweb/lib/providers/payment_methods.dart create mode 100644 frontend/pweb/lib/providers/recipient.dart create mode 100644 frontend/pweb/lib/providers/template.dart create mode 100644 frontend/pweb/lib/providers/two_factor.dart create mode 100644 frontend/pweb/lib/providers/upload_history.dart create mode 100644 frontend/pweb/lib/providers/wallets.dart create mode 100644 frontend/pweb/lib/services/amplitude.dart create mode 100644 frontend/pweb/lib/services/auth.dart create mode 100644 frontend/pweb/lib/services/balance.dart create mode 100644 frontend/pweb/lib/services/payments/payment_methods.dart create mode 100644 frontend/pweb/lib/services/payments/upload_history.dart create mode 100644 frontend/pweb/lib/services/recipient/recipient.dart create mode 100644 frontend/pweb/lib/services/wallets.dart create mode 100644 frontend/pweb/lib/utils/clipboard.dart create mode 100644 frontend/pweb/lib/utils/currency.dart create mode 100644 frontend/pweb/lib/utils/dimensions.dart create mode 100644 frontend/pweb/lib/utils/error/content.dart create mode 100644 frontend/pweb/lib/utils/error/handler.dart create mode 100644 frontend/pweb/lib/utils/error/snackbar.dart create mode 100644 frontend/pweb/lib/utils/error_handler.dart create mode 100644 frontend/pweb/lib/utils/flagged_locale.dart create mode 100644 frontend/pweb/lib/utils/http.dart create mode 100644 frontend/pweb/lib/utils/initials.dart create mode 100644 frontend/pweb/lib/utils/notify.dart create mode 100644 frontend/pweb/lib/utils/payment/dropdown.dart create mode 100644 frontend/pweb/lib/utils/payment/label.dart create mode 100644 frontend/pweb/lib/utils/payment/selector_type.dart create mode 100644 frontend/pweb/lib/utils/share.dart create mode 100644 frontend/pweb/lib/utils/snackbar.dart create mode 100644 frontend/pweb/lib/utils/snapshot_haserror_check.dart create mode 100644 frontend/pweb/lib/utils/text_field_styles.dart create mode 100644 frontend/pweb/lib/utils/time_ago.dart create mode 100644 frontend/pweb/lib/widgets/appbar/app_bar.dart create mode 100644 frontend/pweb/lib/widgets/appbar/notifications.dart create mode 100644 frontend/pweb/lib/widgets/appbar/profile.dart create mode 100644 frontend/pweb/lib/widgets/constrained_form.dart create mode 100644 frontend/pweb/lib/widgets/drawer/avatar.dart create mode 100644 frontend/pweb/lib/widgets/drawer/tiles/dashboard.dart create mode 100644 frontend/pweb/lib/widgets/drawer/tiles/logout.dart create mode 100644 frontend/pweb/lib/widgets/drawer/tiles/settings/permissions.dart create mode 100644 frontend/pweb/lib/widgets/drawer/tiles/settings/profile.dart create mode 100644 frontend/pweb/lib/widgets/drawer/tiles/settings/roles.dart create mode 100644 frontend/pweb/lib/widgets/drawer/tiles/settings/users.dart create mode 100644 frontend/pweb/lib/widgets/drawer/widget.dart create mode 100644 frontend/pweb/lib/widgets/employee/avatar/provider.dart create mode 100644 frontend/pweb/lib/widgets/employee/avatar/widget.dart create mode 100644 frontend/pweb/lib/widgets/employee/provider.dart create mode 100644 frontend/pweb/lib/widgets/employee/tile.dart create mode 100644 frontend/pweb/lib/widgets/error/content.dart create mode 100644 frontend/pweb/lib/widgets/error/snackbar.dart create mode 100644 frontend/pweb/lib/widgets/footer/labels.dart create mode 100644 frontend/pweb/lib/widgets/footer/policies.dart create mode 100644 frontend/pweb/lib/widgets/footer/support.dart create mode 100644 frontend/pweb/lib/widgets/footer/widget.dart create mode 100644 frontend/pweb/lib/widgets/hspacer.dart create mode 100644 frontend/pweb/lib/widgets/logo.dart create mode 100644 frontend/pweb/lib/widgets/password/hint/error.dart create mode 100644 frontend/pweb/lib/widgets/password/hint/full.dart create mode 100644 frontend/pweb/lib/widgets/password/hint/short.dart create mode 100644 frontend/pweb/lib/widgets/password/hint/validation_result.dart create mode 100644 frontend/pweb/lib/widgets/password/hint/widget.dart create mode 100644 frontend/pweb/lib/widgets/password/password.dart create mode 100644 frontend/pweb/lib/widgets/password/verify.dart create mode 100644 frontend/pweb/lib/widgets/protected/widget.dart create mode 100644 frontend/pweb/lib/widgets/search.dart create mode 100644 frontend/pweb/lib/widgets/sidebar/destinations.dart create mode 100644 frontend/pweb/lib/widgets/sidebar/page.dart create mode 100644 frontend/pweb/lib/widgets/sidebar/side_menu.dart create mode 100644 frontend/pweb/lib/widgets/sidebar/sidebar.dart create mode 100644 frontend/pweb/lib/widgets/sidebar/user.dart create mode 100644 frontend/pweb/lib/widgets/stats/card.dart create mode 100644 frontend/pweb/lib/widgets/text_field.dart create mode 100644 frontend/pweb/lib/widgets/username.dart create mode 100644 frontend/pweb/lib/widgets/vspacer.dart create mode 100644 frontend/pweb/linux/.gitignore create mode 100644 frontend/pweb/linux/CMakeLists.txt create mode 100644 frontend/pweb/linux/flutter/CMakeLists.txt create mode 100644 frontend/pweb/linux/flutter/generated_plugin_registrant.cc create mode 100644 frontend/pweb/linux/flutter/generated_plugin_registrant.h create mode 100644 frontend/pweb/linux/flutter/generated_plugins.cmake create mode 100644 frontend/pweb/linux/runner/CMakeLists.txt create mode 100644 frontend/pweb/linux/runner/main.cc create mode 100644 frontend/pweb/linux/runner/my_application.cc create mode 100644 frontend/pweb/linux/runner/my_application.h create mode 100644 frontend/pweb/macos/.gitignore create mode 100644 frontend/pweb/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 frontend/pweb/macos/Flutter/Flutter-Release.xcconfig create mode 100644 frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 frontend/pweb/macos/Podfile create mode 100644 frontend/pweb/macos/Runner.xcodeproj/project.pbxproj create mode 100644 frontend/pweb/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/pweb/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 frontend/pweb/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 frontend/pweb/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 frontend/pweb/macos/Runner/AppDelegate.swift create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 frontend/pweb/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 frontend/pweb/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 frontend/pweb/macos/Runner/Configs/Debug.xcconfig create mode 100644 frontend/pweb/macos/Runner/Configs/Release.xcconfig create mode 100644 frontend/pweb/macos/Runner/Configs/Warnings.xcconfig create mode 100644 frontend/pweb/macos/Runner/DebugProfile.entitlements create mode 100644 frontend/pweb/macos/Runner/Info.plist create mode 100644 frontend/pweb/macos/Runner/MainFlutterWindow.swift create mode 100644 frontend/pweb/macos/Runner/Release.entitlements create mode 100644 frontend/pweb/macos/RunnerTests/RunnerTests.swift create mode 100644 frontend/pweb/pubspec.lock create mode 100644 frontend/pweb/pubspec.yaml create mode 100644 frontend/pweb/resources/logo.png create mode 100644 frontend/pweb/resources/logo.si create mode 100644 frontend/pweb/test/widget_test.dart create mode 100644 frontend/pweb/untranslated.txt create mode 100644 frontend/pweb/web/favicon.png create mode 100644 frontend/pweb/web/icons/Icon-192.png create mode 100644 frontend/pweb/web/icons/Icon-512.png create mode 100644 frontend/pweb/web/icons/Icon-maskable-192.png create mode 100644 frontend/pweb/web/icons/Icon-maskable-512.png create mode 100644 frontend/pweb/web/index.html create mode 100644 frontend/pweb/web/manifest.json create mode 100644 frontend/pweb/windows/.gitignore create mode 100644 frontend/pweb/windows/CMakeLists.txt create mode 100644 frontend/pweb/windows/flutter/CMakeLists.txt create mode 100644 frontend/pweb/windows/flutter/generated_plugin_registrant.cc create mode 100644 frontend/pweb/windows/flutter/generated_plugin_registrant.h create mode 100644 frontend/pweb/windows/flutter/generated_plugins.cmake create mode 100644 frontend/pweb/windows/runner/CMakeLists.txt create mode 100644 frontend/pweb/windows/runner/Runner.rc create mode 100644 frontend/pweb/windows/runner/flutter_window.cpp create mode 100644 frontend/pweb/windows/runner/flutter_window.h create mode 100644 frontend/pweb/windows/runner/main.cpp create mode 100644 frontend/pweb/windows/runner/resource.h create mode 100644 frontend/pweb/windows/runner/resources/app_icon.ico create mode 100644 frontend/pweb/windows/runner/runner.exe.manifest create mode 100644 frontend/pweb/windows/runner/utils.cpp create mode 100644 frontend/pweb/windows/runner/utils.h create mode 100644 frontend/pweb/windows/runner/win32_window.cpp create mode 100644 frontend/pweb/windows/runner/win32_window.h diff --git a/.DS_Store b/.DS_Store index 413e071a8796afef319a87d679f7dd7dd02e84e9..9a280bb577904be03cdc7b9982d25a54499f2a1a 100644 GIT binary patch delta 42 ycmZn(XbG6$&&atkU^hP_=Vl&(n@pRv#qMx!Y;a@V%&zc@WwNsLg3Zq*l9>QF4-Uit delta 323 zcmZn(XbG6$&nU1lU^hRbz-AtSn@sG?3`q<|3`vs@h_*ADPM#;=$O`6f-X{E-u^uX! zlWrKCoS$33fB;aL+bT$GoSpO?P* Xp3q~?&Fl)lSSJ4!SuzM*^MDZm4JlQC diff --git a/b2b/hi.dart b/b2b/hi.dart deleted file mode 100644 index 2d9e1be..0000000 --- a/b2b/hi.dart +++ /dev/null @@ -1 +0,0 @@ -// Hi \ No newline at end of file diff --git a/frontend/pshared/.gitignore b/frontend/pshared/.gitignore new file mode 100644 index 0000000..105c27d --- /dev/null +++ b/frontend/pshared/.gitignore @@ -0,0 +1,16 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +build/ +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +*.g.dart +lib/generated +untranslated.txt + +.flutter-plugins +.flutter-plugins-dependencies +devtools_options.yaml \ No newline at end of file diff --git a/frontend/pshared/CHANGELOG.md b/frontend/pshared/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/frontend/pshared/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/frontend/pshared/README.md b/frontend/pshared/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/frontend/pshared/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/frontend/pshared/analysis_options.yaml b/frontend/pshared/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/frontend/pshared/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/frontend/pshared/assets/flag_of_catalonia.si b/frontend/pshared/assets/flag_of_catalonia.si new file mode 100644 index 0000000000000000000000000000000000000000..3568e066c9b8540f045166eb1c98b588bbc0694e GIT binary patch literal 169 zcmdn6L5`h)frpuafq@B%rGYdMG&q1MFw@Hg%x(bD><~7XE^tDq+v3CkmWR%R~XFaQ7oej2?1 literal 0 HcmV?d00001 diff --git a/frontend/pshared/l10n.yaml b/frontend/pshared/l10n.yaml new file mode 100644 index 0000000..11495f3 --- /dev/null +++ b/frontend/pshared/l10n.yaml @@ -0,0 +1,7 @@ +arb-dir: lib/l10n +template-arb-file: ps_en.arb +output-dir: lib/generated/i18n +output-localization-file: ps_localizations.dart +output-class: PSLocalizations +synthetic-package: false +untranslated-messages-file: untranslated.txt diff --git a/frontend/pshared/lib/api/errors/authorization_failed.dart b/frontend/pshared/lib/api/errors/authorization_failed.dart new file mode 100644 index 0000000..7acc439 --- /dev/null +++ b/frontend/pshared/lib/api/errors/authorization_failed.dart @@ -0,0 +1,2 @@ +class AuthorizationFailed implements Exception { +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/errors/failed_to_read_image.dart b/frontend/pshared/lib/api/errors/failed_to_read_image.dart new file mode 100644 index 0000000..5b2b533 --- /dev/null +++ b/frontend/pshared/lib/api/errors/failed_to_read_image.dart @@ -0,0 +1,2 @@ +class ErrorFailedToReadImage implements Exception { +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/errors/unauthorized.dart b/frontend/pshared/lib/api/errors/unauthorized.dart new file mode 100644 index 0000000..220fc68 --- /dev/null +++ b/frontend/pshared/lib/api/errors/unauthorized.dart @@ -0,0 +1,2 @@ +class ErrorUnauthorized implements Exception { +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/errors/upload_failed.dart b/frontend/pshared/lib/api/errors/upload_failed.dart new file mode 100644 index 0000000..11c33dc --- /dev/null +++ b/frontend/pshared/lib/api/errors/upload_failed.dart @@ -0,0 +1,2 @@ +class ErrorUploadFailed implements Exception { +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/requests/change_password.dart b/frontend/pshared/lib/api/requests/change_password.dart new file mode 100644 index 0000000..5bdb21f --- /dev/null +++ b/frontend/pshared/lib/api/requests/change_password.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'change_password.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class ChangePassword { + @JsonKey(name: 'old') + final String oldPassword; + + @JsonKey(name: 'new') + final String newPassword; + + const ChangePassword({required this.oldPassword, required this.newPassword}); + + factory ChangePassword.fromJson(Map json) => _$ChangePasswordFromJson(json); + Map toJson() => _$ChangePasswordToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/change_role.dart b/frontend/pshared/lib/api/requests/change_role.dart new file mode 100644 index 0000000..adbeab9 --- /dev/null +++ b/frontend/pshared/lib/api/requests/change_role.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'change_role.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class ChangeRole { + final String accountRef; + final String newRoleDescriptionRef; + + const ChangeRole({ + required this.accountRef, + required this.newRoleDescriptionRef, + }); + + factory ChangeRole.fromJson(Map json) => _$ChangeRoleFromJson(json); + Map toJson() => _$ChangeRoleToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/file_upload.dart b/frontend/pshared/lib/api/requests/file_upload.dart new file mode 100644 index 0000000..2864b0a --- /dev/null +++ b/frontend/pshared/lib/api/requests/file_upload.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'file_upload.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class FileUpload { + + final String objRef; + + const FileUpload({ required this.objRef }); + + factory FileUpload.fromJson(Map json) => _$FileUploadFromJson(json); + Map toJson() => _$FileUploadToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/requests/login.dart b/frontend/pshared/lib/api/requests/login.dart new file mode 100644 index 0000000..532d132 --- /dev/null +++ b/frontend/pshared/lib/api/requests/login.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class LoginRequest { + final String login; + final String password; + final String locale; + final String clientId; + final String deviceId; + + const LoginRequest({ + required this.login, + required this.password, + required this.locale, + required this.clientId, + required this.deviceId, + }); + + factory LoginRequest.fromJson(Map json) => _$LoginRequestFromJson(json); + Map toJson() => _$LoginRequestToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/permissions/change_policies.dart b/frontend/pshared/lib/api/requests/permissions/change_policies.dart new file mode 100644 index 0000000..b49f7cd --- /dev/null +++ b/frontend/pshared/lib/api/requests/permissions/change_policies.dart @@ -0,0 +1,38 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/permissions/data/policy.dart'; +import 'package:pshared/data/mapper/permissions/data/policy.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; + +part 'change_policies.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class PoliciesChangeRequest { + final List? add; + final List? remove; + + const PoliciesChangeRequest({ + this.add, + this.remove, + }); + + factory PoliciesChangeRequest.add({required List policies}) => PoliciesChangeRequest( + add: policies.map((policy) => policy.toDTO()).toList(), + ); + + factory PoliciesChangeRequest.remove({required List policies}) => PoliciesChangeRequest( + remove: policies.map((policy) => policy.toDTO()).toList(), + ); + + factory PoliciesChangeRequest.change({ + required List add, + required List remove, + }) => PoliciesChangeRequest( + add: add.map((policy) => policy.toDTO()).toList(), + remove: remove.map((policy) => policy.toDTO()).toList(), + ); + + factory PoliciesChangeRequest.fromJson(Map json) => _$PoliciesChangeRequestFromJson(json); + Map toJson() => _$PoliciesChangeRequestToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/signup.dart b/frontend/pshared/lib/api/requests/signup.dart new file mode 100644 index 0000000..98a817a --- /dev/null +++ b/frontend/pshared/lib/api/requests/signup.dart @@ -0,0 +1,42 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'signup.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class SignupRequest { + final String name; + final String login; + final String password; + final String locale; + final String organizationName; + final String organizationTimeZone; + + const SignupRequest({ + required this.name, + required this.login, + required this.password, + required this.locale, + required this.organizationName, + required this.organizationTimeZone, + }); + + factory SignupRequest.build({ + required String name, + required String login, + required String password, + required String locale, + required String organizationName, + required String organizationTimeZone, + }) => SignupRequest( + name: name, + login: login, + password: password, + locale: locale, + organizationName: organizationName, + organizationTimeZone: organizationTimeZone, + ); + + factory SignupRequest.fromJson(Map json) => _$SignupRequestFromJson(json); + Map toJson() => _$SignupRequestToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/tokens/access_refresh.dart b/frontend/pshared/lib/api/requests/tokens/access_refresh.dart new file mode 100644 index 0000000..6f2b719 --- /dev/null +++ b/frontend/pshared/lib/api/requests/tokens/access_refresh.dart @@ -0,0 +1,4 @@ +import 'package:pshared/api/requests/tokens/refresh_rotate.dart'; + + +typedef AccessTokenRefreshRequest = RotateRefreshTokenRequest; \ No newline at end of file diff --git a/frontend/pshared/lib/api/requests/tokens/refresh_rotate.dart b/frontend/pshared/lib/api/requests/tokens/refresh_rotate.dart new file mode 100644 index 0000000..9d70659 --- /dev/null +++ b/frontend/pshared/lib/api/requests/tokens/refresh_rotate.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/requests/tokens/session_id.dart'; + +part 'refresh_rotate.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class RotateRefreshTokenRequest extends SessionID { + final String token; + + const RotateRefreshTokenRequest({ + required this.token, + required super.clientId, + required super.deviceId, + }); + + factory RotateRefreshTokenRequest.fromJson(Map json) => _$RotateRefreshTokenRequestFromJson(json); + @override + Map toJson() => _$RotateRefreshTokenRequestToJson(this); +} diff --git a/frontend/pshared/lib/api/requests/tokens/session_id.dart b/frontend/pshared/lib/api/requests/tokens/session_id.dart new file mode 100644 index 0000000..9040739 --- /dev/null +++ b/frontend/pshared/lib/api/requests/tokens/session_id.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'session_id.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class SessionID { + final String clientId; + final String deviceId; + + const SessionID({ + required this.clientId, + required this.deviceId, + }); + + factory SessionID.fromJson(Map json) => _$SessionIDFromJson(json); + Map toJson() => _$SessionIDToJson(this); +} diff --git a/frontend/pshared/lib/api/responses/account.dart b/frontend/pshared/lib/api/responses/account.dart new file mode 100644 index 0000000..8733871 --- /dev/null +++ b/frontend/pshared/lib/api/responses/account.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/dto/account/account.dart'; + +part 'account.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class AccountResponse { + final AccountDTO account; + final TokenData accessToken; + + const AccountResponse({required this.accessToken, required this.account}); + + factory AccountResponse.fromJson(Map json) => _$AccountResponseFromJson(json); + Map toJson() => _$AccountResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/base.dart b/frontend/pshared/lib/api/responses/base.dart new file mode 100644 index 0000000..63c26f5 --- /dev/null +++ b/frontend/pshared/lib/api/responses/base.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/token.dart'; + +part 'base.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class BaseAuthorizedResponse { + final TokenData accessToken; + + const BaseAuthorizedResponse({required this.accessToken}); + + factory BaseAuthorizedResponse.fromJson(Map json) => _$BaseAuthorizedResponseFromJson(json); + Map toJson() => _$BaseAuthorizedResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/employees.dart b/frontend/pshared/lib/api/responses/employees.dart new file mode 100644 index 0000000..5d9fccf --- /dev/null +++ b/frontend/pshared/lib/api/responses/employees.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/dto/account/account.dart'; + +part 'employees.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class EmployeesResponse { + final List accounts; + final TokenData accessToken; + + const EmployeesResponse({required this.accessToken, required this.accounts}); + + factory EmployeesResponse.fromJson(Map json) => _$EmployeesResponseFromJson(json); + Map toJson() => _$EmployeesResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/error/connectivity.dart b/frontend/pshared/lib/api/responses/error/connectivity.dart new file mode 100644 index 0000000..9795f4f --- /dev/null +++ b/frontend/pshared/lib/api/responses/error/connectivity.dart @@ -0,0 +1,13 @@ +class ConnectivityError implements Exception { + final int? code; + final String message; + + const ConnectivityError({this.code, required this.message}); + + @override + String toString() { + return code == null + ? 'Error response, message: $message)' + : 'Error response (code: $code, message: $message)'; + } +} diff --git a/frontend/pshared/lib/api/responses/error/server.dart b/frontend/pshared/lib/api/responses/error/server.dart new file mode 100644 index 0000000..992ae4d --- /dev/null +++ b/frontend/pshared/lib/api/responses/error/server.dart @@ -0,0 +1,43 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'server.g.dart'; + + +@JsonSerializable() +class ErrorResponse implements Exception { + final int code; + final String details; + final String source; + final String error; + + const ErrorResponse({ + required this.code, + required this.details, + required this.error, + required this.source, + }); + + @override + String toString() { + final buffer = StringBuffer('Error response (code: $code'); + + if (details.isNotEmpty) { + buffer.write(', details: $details'); + } + + if (error.isNotEmpty) { + buffer.write(', error: $error'); + } + + if (source.isNotEmpty) { + buffer.write(', source: $source'); + } + + buffer.write(')'); + + return buffer.toString(); + } + + factory ErrorResponse.fromJson(Map json) => _$ErrorResponseFromJson(json); + Map toJson() => _$ErrorResponseToJson(this); +} diff --git a/frontend/pshared/lib/api/responses/file_uploaded.dart b/frontend/pshared/lib/api/responses/file_uploaded.dart new file mode 100644 index 0000000..d178e10 --- /dev/null +++ b/frontend/pshared/lib/api/responses/file_uploaded.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'file_uploaded.g.dart'; + + +@JsonSerializable() +class FileUploaded { + + final String url; + + const FileUploaded({ required this.url }); + + factory FileUploaded.fromJson(Map json) => _$FileUploadedFromJson(json); + Map toJson() => _$FileUploadedToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/login.dart b/frontend/pshared/lib/api/responses/login.dart new file mode 100644 index 0000000..95a9ed2 --- /dev/null +++ b/frontend/pshared/lib/api/responses/login.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/account.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/dto/account/account.dart'; + +part 'login.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class LoginResponse extends AccountResponse { + final TokenData refreshToken; + + const LoginResponse({required super.accessToken, required super.account, required this.refreshToken}); + + factory LoginResponse.fromJson(Map json) => _$LoginResponseFromJson(json); + @override + Map toJson() => _$LoginResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/message.dart b/frontend/pshared/lib/api/responses/message.dart new file mode 100644 index 0000000..97ba1d1 --- /dev/null +++ b/frontend/pshared/lib/api/responses/message.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/type.dart'; + +part 'message.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class HTTPMessage { + + @JsonKey(fromJson: MessageTypeExtension.fromJson, toJson: MessageTypeExtension.toJson) + final MessageType status; + final Map data; + + const HTTPMessage({ required this.data, required this.status }); + + factory HTTPMessage.fromJson(Map json) => _$HTTPMessageFromJson(json); + Map toJson() => _$HTTPMessageToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/organization.dart b/frontend/pshared/lib/api/responses/organization.dart new file mode 100644 index 0000000..ea05c9b --- /dev/null +++ b/frontend/pshared/lib/api/responses/organization.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/base.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/dto/organization.dart'; + +part 'organization.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class OrganizationResponse extends BaseAuthorizedResponse { + final List organizations; + + const OrganizationResponse({required super.accessToken, required this.organizations}); + + factory OrganizationResponse.fromJson(Map json) => _$OrganizationResponseFromJson(json); + @override + Map toJson() => _$OrganizationResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/policies.dart b/frontend/pshared/lib/api/responses/policies.dart new file mode 100644 index 0000000..d097339 --- /dev/null +++ b/frontend/pshared/lib/api/responses/policies.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/api/responses/base.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/data/dto/permissions/data/permissions.dart'; +import 'package:pshared/data/dto/permissions/description/description.dart'; + +part 'policies.g.dart'; + + +@JsonSerializable(explicitToJson: true) +class PoliciesResponse extends BaseAuthorizedResponse { + final PermissionsDescriptionDTO descriptions; + final PermissionsDataDTO permissions; + + const PoliciesResponse({required this.descriptions, required this.permissions, required super.accessToken}); + + factory PoliciesResponse.fromJson(Map json) => _$PoliciesResponseFromJson(json); + @override + Map toJson() => _$PoliciesResponseToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/token.dart b/frontend/pshared/lib/api/responses/token.dart new file mode 100644 index 0000000..f485d46 --- /dev/null +++ b/frontend/pshared/lib/api/responses/token.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'token.g.dart'; + + +@JsonSerializable() +class TokenData { + final String token; + final DateTime expiration; + + const TokenData({required this.token, required this.expiration}); + + factory TokenData.fromJson(Map json) => _$TokenDataFromJson(json); + Map toJson() => _$TokenDataToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/api/responses/type.dart b/frontend/pshared/lib/api/responses/type.dart new file mode 100644 index 0000000..98673b8 --- /dev/null +++ b/frontend/pshared/lib/api/responses/type.dart @@ -0,0 +1,32 @@ + +enum MessageType { + success, + error, + request +} + +extension MessageTypeExtension on MessageType { + static String toJson(MessageType value) { + switch (value) { + case MessageType.success: + return 'success'; + case MessageType.error: + return 'error'; + case MessageType.request: + return 'request'; + } + } + + static MessageType fromJson(String json) { + switch (json) { + case 'success': + return MessageType.success; + case 'error': + return MessageType.error; + case 'request': + return MessageType.request; + default: + throw ArgumentError('Unknown HTTPMType string: $json'); + } + } +} diff --git a/frontend/pshared/lib/config/common.dart b/frontend/pshared/lib/config/common.dart new file mode 100644 index 0000000..b0f6de5 --- /dev/null +++ b/frontend/pshared/lib/config/common.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + + +class CommonConstants { + static String apiProto = const String.fromEnvironment('API_PROTO', defaultValue: 'http'); + static String apiHost = const String.fromEnvironment('API_HOST', defaultValue: 'localhost'); + // static String apiHost = 'localhost'; + // static String apiHost = '10.0.2.2'; + static String apiEndpoint = '/api/v1'; + static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79'; + static String amplitudeServerZone = 'EU'; + static Locale defaultLocale = const Locale('en'); + static String defaultCurrency = 'EUR'; + static int defaultDimensionLength = 500; + static String clientId = ''; + static String wsProto = 'ws'; + static String wsEndpoint = '/ws'; + static Color themeColor = Color.fromARGB(255, 80, 63, 224); + static String nilObjectRef = '000000000000000000000000'; + + // Public getters for shared properties + static String get serviceUrl => '$apiProto://$apiHost'; + static String get apiUrl => '$serviceUrl$apiEndpoint'; + static String get wsUrl => '$wsProto://$apiHost$apiEndpoint$wsEndpoint'; + static const String accessTokenStorageKey = 'access_token'; + static const String refreshTokenStorageKey = 'refresh_token'; + static const String currentOrgKey = 'current_org'; + static const String deviceIdStorageKey = 'device_id'; + + // Method to apply the configuration, called by platform-specific implementations + static void applyConfiguration(Map configJson) { + apiProto = configJson['apiProto'] ?? apiProto; + apiHost = configJson['apiHost'] ?? apiHost; + apiEndpoint = configJson['apiEndpoint'] ?? apiEndpoint; + amplitudeSecret = configJson['amplitudeSecret'] ?? amplitudeSecret; + amplitudeServerZone = configJson['amplitudeServerZone'] ?? amplitudeServerZone; + defaultLocale = Locale(configJson['defaultLocale'] ?? defaultLocale.languageCode); + defaultCurrency = configJson['defaultCurrency'] ?? defaultCurrency; + wsProto = configJson['wsProto'] ?? wsProto; + wsEndpoint = configJson['wsEndpoint'] ?? wsEndpoint; + defaultDimensionLength = configJson['defaultDimensionLength'] ?? defaultDimensionLength; + clientId = configJson['clientId'] ?? clientId; + if (configJson.containsKey('themeColor')) { + themeColor = Color(int.parse(configJson['themeColor'])); + } + } +} diff --git a/frontend/pshared/lib/config/constants.dart b/frontend/pshared/lib/config/constants.dart new file mode 100644 index 0000000..f88f154 --- /dev/null +++ b/frontend/pshared/lib/config/constants.dart @@ -0,0 +1,2 @@ +export 'mobile.dart' + if (dart.library.html) 'web.dart'; \ No newline at end of file diff --git a/frontend/pshared/lib/config/mobile.dart b/frontend/pshared/lib/config/mobile.dart new file mode 100644 index 0000000..ec5f56d --- /dev/null +++ b/frontend/pshared/lib/config/mobile.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:pshared/config/common.dart'; + + +class Constants extends CommonConstants { + static const String _clientIdMobile = 'com.profee.pay.mobile-3f9c3b76-2f89-4e9e-95a2-1a5b705b7a1d'; + + static Locale get defaultLocale => CommonConstants.defaultLocale; + static String get clientId => _clientIdMobile; + static String get accessTokenStorageKey => CommonConstants.accessTokenStorageKey; + static String get refreshTokenStorageKey => CommonConstants.refreshTokenStorageKey; + static String get currentOrgKey => CommonConstants.currentOrgKey; + static String get apiUrl => CommonConstants.apiUrl; + static String get serviceUrl => CommonConstants.serviceUrl; + static int get defaultDimensionLength => CommonConstants.defaultDimensionLength; + static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey; + static String get nilObjectRef => CommonConstants.nilObjectRef; + static Color get themeColor => CommonConstants.themeColor; + + static Future initialize() async { + var configFile = File('./config/config.json'); + if (await configFile.exists()) { + var configJson = jsonDecode(await configFile.readAsString()); + CommonConstants.applyConfiguration({ + ...configJson, + 'clientId': configJson['clientId'] ?? _clientIdMobile, + }); + } else { + CommonConstants.clientId = _clientIdMobile; + } + } +} diff --git a/frontend/pshared/lib/config/web.dart b/frontend/pshared/lib/config/web.dart new file mode 100644 index 0000000..cc49d85 --- /dev/null +++ b/frontend/pshared/lib/config/web.dart @@ -0,0 +1,75 @@ +import 'dart:ui'; + +import 'dart:js_interop'; + +import 'package:pshared/config/common.dart'; + + +/// Bind to the global JS `appConfig` (if it exists). +@JS() +external AppConfig? get appConfig; + +/// A staticInterop class for the JS object returned by `appConfig`. +@JS() +@staticInterop +class AppConfig {} + +/// Extension methods to expose each property on `AppConfig`. +extension AppConfigExtension on AppConfig { + external String? get apiProto; + external String? get apiHost; + external String? get apiEndpoint; + external String? get amplitudeSecret; + external String? get amplitudeServerZone; + external String? get defaultLocale; + external String? get wsProto; + external String? get wsEndpoint; + external int? get defaultDimensionLength; + external String? get themeColor; + external String? get clientId; +} + +class Constants extends CommonConstants { + static const String _clientIdWeb = 'com.profee.pay.web-4b6e8a0f-9b5c-4f57-b3a6-3c456e9bb2cd'; + + // Just re-expose these from CommonConstants: + static Locale get defaultLocale => CommonConstants.defaultLocale; + static String get clientId => _clientIdWeb; + static String get accessTokenStorageKey => CommonConstants.accessTokenStorageKey; + static String get refreshTokenStorageKey => CommonConstants.refreshTokenStorageKey; + static String get currentOrgKey => CommonConstants.currentOrgKey; + static String get apiUrl => CommonConstants.apiUrl; + static String get serviceUrl => CommonConstants.serviceUrl; + static int get defaultDimensionLength => CommonConstants.defaultDimensionLength; + static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey; + static String get nilObjectRef => CommonConstants.nilObjectRef; + static Color get themeColor => CommonConstants.themeColor; + + static Future initialize() async { + // Try to grab the JS `appConfig` if it exists: + final config = appConfig; + + if (config != null) { + // Build a Dart Map from the JS object’s properties: + final configJson = { + 'apiProto': config.apiProto, + 'apiHost': config.apiHost, + 'apiEndpoint': config.apiEndpoint, + 'amplitudeSecret': config.amplitudeSecret, + 'amplitudeServerZone': config.amplitudeServerZone, + 'defaultLocale': config.defaultLocale, + 'wsProto': config.wsProto, + 'wsEndpoint': config.wsEndpoint, + 'defaultDimensionLength': config.defaultDimensionLength, + 'themeColor': config.themeColor, + // If the JS side didn’t supply a clientId, fall back to our Dart constant + 'clientId': config.clientId ?? _clientIdWeb, + }; + + CommonConstants.applyConfiguration(configJson); + } else { + // No appConfig on JS side → just use the default Dart client ID. + CommonConstants.clientId = _clientIdWeb; + } + } +} diff --git a/frontend/pshared/lib/data/dto/account/account.dart b/frontend/pshared/lib/data/dto/account/account.dart new file mode 100644 index 0000000..b1a080b --- /dev/null +++ b/frontend/pshared/lib/data/dto/account/account.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/account/base.dart'; + +part 'account.g.dart'; + + +@JsonSerializable() +class AccountDTO extends AccountBaseDTO { + final String login; + + const AccountDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required super.name, + required super.avatarUrl, + required super.locale, + required this.login, + }); + + factory AccountDTO.fromJson(Map json) => _$AccountDTOFromJson(json); + @override + Map toJson() => _$AccountDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/account/base.dart b/frontend/pshared/lib/data/dto/account/base.dart new file mode 100644 index 0000000..0ae451d --- /dev/null +++ b/frontend/pshared/lib/data/dto/account/base.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/storable.dart'; + +part 'base.g.dart'; + + +@JsonSerializable() +class AccountBaseDTO extends StorableDTO { + final String name; + final String locale; + final String? avatarUrl; + + const AccountBaseDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required this.name, + required this.avatarUrl, + required this.locale, + }); + + factory AccountBaseDTO.fromJson(Map json) => _$AccountBaseDTOFromJson(json); + + @override + Map toJson() => _$AccountBaseDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/organization.dart b/frontend/pshared/lib/data/dto/organization.dart new file mode 100644 index 0000000..9e5cc76 --- /dev/null +++ b/frontend/pshared/lib/data/dto/organization.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/data/dto/storable.dart'; + +part 'organization.g.dart'; + + +@JsonSerializable() +class OrganizationDTO extends StorableDTO { + final String timeZone; + final String? logoUrl; + + const OrganizationDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required this.timeZone, + this.logoUrl, + }); + + factory OrganizationDTO.fromJson(Map json) => _$OrganizationDTOFromJson(json); + @override + Map toJson() => _$OrganizationDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/organization/description.dart b/frontend/pshared/lib/data/dto/organization/description.dart new file mode 100644 index 0000000..2814f8b --- /dev/null +++ b/frontend/pshared/lib/data/dto/organization/description.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'description.g.dart'; + + +@JsonSerializable() +class OrganizationDescriptionDTO { + final String? logoUrl; + + const OrganizationDescriptionDTO({ + this.logoUrl, + }); + + factory OrganizationDescriptionDTO.fromJson(Map json) => _$OrganizationDescriptionDTOFromJson(json); + Map toJson() => _$OrganizationDescriptionDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/permissions/access.dart b/frontend/pshared/lib/data/dto/permissions/access.dart new file mode 100644 index 0000000..a69d970 --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/access.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/permissions/data/permissions.dart'; +import 'package:pshared/data/dto/permissions/description/description.dart'; + +part 'access.g.dart'; + + +@JsonSerializable() +class UserAccessDTO { + final PermissionsDescriptionDTO descriptions; + final PermissionsDataDTO permissions; + + const UserAccessDTO({ + required this.descriptions, + required this.permissions, + }); + + factory UserAccessDTO.fromJson(Map json) => _$UserAccessDTOFromJson(json); + Map toJson() => _$UserAccessDTOToJson(this); +} + diff --git a/frontend/pshared/lib/data/dto/permissions/action_effect.dart b/frontend/pshared/lib/data/dto/permissions/action_effect.dart new file mode 100644 index 0000000..fddfd44 --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/action_effect.dart @@ -0,0 +1,19 @@ +// data/action_effect_dto.dart +import 'package:json_annotation/json_annotation.dart'; + +part 'action_effect.g.dart'; + + +@JsonSerializable() +class ActionEffectDTO { + final String action; + final String effect; + + const ActionEffectDTO({ + required this.action, + required this.effect, + }); + + factory ActionEffectDTO.fromJson(Map json) => _$ActionEffectDTOFromJson(json); + Map toJson() => _$ActionEffectDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/permissions/data/permission.dart b/frontend/pshared/lib/data/dto/permissions/data/permission.dart new file mode 100644 index 0000000..2e4a3b7 --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/data/permission.dart @@ -0,0 +1,28 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/permissions/action_effect.dart'; + +part 'permission.g.dart'; + + +@JsonSerializable() +class PermissionDTO { + final String roleDescriptionRef; + final String organizationRef; + final String descriptionRef; + final String? objectRef; + final ActionEffectDTO effect; + final String accountRef; + + const PermissionDTO({ + required this.roleDescriptionRef, + required this.organizationRef, + required this.descriptionRef, + required this.objectRef, + required this.effect, + required this.accountRef, + }); + + factory PermissionDTO.fromJson(Map json) => _$PermissionDTOFromJson(json); + Map toJson() => _$PermissionDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/permissions/data/permissions.dart b/frontend/pshared/lib/data/dto/permissions/data/permissions.dart new file mode 100644 index 0000000..8759baf --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/data/permissions.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/permissions/data/permission.dart'; +import 'package:pshared/data/dto/permissions/data/policy.dart'; +import 'package:pshared/data/dto/permissions/data/role.dart'; + +part 'permissions.g.dart'; + + +@JsonSerializable() +class PermissionsDataDTO { + final List roles; + final List policies; + final List permissions; + + const PermissionsDataDTO({ + required this.roles, + required this.policies, + required this.permissions, + }); + + factory PermissionsDataDTO.fromJson(Map json) => _$PermissionsDataDTOFromJson(json); + Map toJson() => _$PermissionsDataDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/permissions/data/policy.dart b/frontend/pshared/lib/data/dto/permissions/data/policy.dart new file mode 100644 index 0000000..a3245de --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/data/policy.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/permissions/action_effect.dart'; + +part 'policy.g.dart'; + + +@JsonSerializable() +class PolicyDTO { + final String roleDescriptionRef; + final String organizationRef; + final String descriptionRef; + final String? objectRef; + final ActionEffectDTO effect; + + const PolicyDTO({ + required this.roleDescriptionRef, + required this.organizationRef, + required this.descriptionRef, + required this.objectRef, + required this.effect, + }); + + factory PolicyDTO.fromJson(Map json) => _$PolicyDTOFromJson(json); + Map toJson() => _$PolicyDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/permissions/data/role.dart b/frontend/pshared/lib/data/dto/permissions/data/role.dart new file mode 100644 index 0000000..d9926de --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/data/role.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'role.g.dart'; + + +@JsonSerializable() +class RoleDTO { + final String accountRef; + final String organizationRef; + final String descriptionRef; + + const RoleDTO({ + required this.accountRef, + required this.descriptionRef, + required this.organizationRef, + }); + + factory RoleDTO.fromJson(Map json) => _$RoleDTOFromJson(json); + Map toJson() => _$RoleDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/permissions/description/description.dart b/frontend/pshared/lib/data/dto/permissions/description/description.dart new file mode 100644 index 0000000..58e8cc8 --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/description/description.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'package:pshared/data/dto/permissions/description/policy.dart'; +import 'package:pshared/data/dto/permissions/description/role.dart'; + +part 'description.g.dart'; + + +@JsonSerializable() +class PermissionsDescriptionDTO { + final List roles; + final List policies; + + const PermissionsDescriptionDTO({ + required this.roles, + required this.policies, + }); + + factory PermissionsDescriptionDTO.fromJson(Map json) => _$PermissionsDescriptionDTOFromJson(json); + Map toJson() => _$PermissionsDescriptionDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/permissions/description/policy.dart b/frontend/pshared/lib/data/dto/permissions/description/policy.dart new file mode 100644 index 0000000..04f4e43 --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/description/policy.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/data/dto/storable.dart'; +import 'package:pshared/models/resources.dart'; + +part 'policy.g.dart'; + + +@JsonSerializable() +class PolicyDescriptionDTO extends StorableDTO { + final List? resourceTypes; + final String? organizationRef; + + const PolicyDescriptionDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required this.resourceTypes, + required this.organizationRef, + }); + + factory PolicyDescriptionDTO.fromJson(Map json) => _$PolicyDescriptionDTOFromJson(json); + + @override + Map toJson() => _$PolicyDescriptionDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/dto/permissions/description/role.dart b/frontend/pshared/lib/data/dto/permissions/description/role.dart new file mode 100644 index 0000000..8b92caf --- /dev/null +++ b/frontend/pshared/lib/data/dto/permissions/description/role.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/data/dto/storable.dart'; + +part 'role.g.dart'; + + +@JsonSerializable() +class RoleDescriptionDTO extends StorableDTO { + final String organizationRef; + + const RoleDescriptionDTO({ + required super.id, + required super.createdAt, + required super.updatedAt, + required this.organizationRef, + }); + + factory RoleDescriptionDTO.fromJson(Map json) => _$RoleDescriptionDTOFromJson(json); + + @override + Map toJson() => _$RoleDescriptionDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/dto/pfe/services.dart b/frontend/pshared/lib/data/dto/pfe/services.dart new file mode 100644 index 0000000..e69de29 diff --git a/frontend/pshared/lib/data/dto/storable.dart b/frontend/pshared/lib/data/dto/storable.dart new file mode 100644 index 0000000..b189dff --- /dev/null +++ b/frontend/pshared/lib/data/dto/storable.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'storable.g.dart'; + + +@JsonSerializable() +class StorableDTO { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + + const StorableDTO({ + required this.id, + required this.createdAt, + required this.updatedAt, + }); + + factory StorableDTO.fromJson(Map json) => _$StorableDTOFromJson(json); + Map toJson() => _$StorableDTOToJson(this); +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/mapper/.DS_Store b/frontend/pshared/lib/data/mapper/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..226f3675cde83e7f562905af626f1881dfaae707 GIT binary patch literal 6148 zcmeHKJ5B>J5S@WYtb~M8Qu+pv8<=Q0K`s!INKhnfilpDufke+)DB(TN1hI+>8W2J= zk^RP=kG)Td?GX{3?pG6$v4}KiM5R&@PFGDQ?mPl&$+4G5*{$y1KB#Pc8xS4!9Xw&3hM3JDbv+8%7@t1OvgqCj&em5*jf(_J-x?KvzltAfM4iptF`>OmfVQy&)_RwNRji zs+SmQ;h0bEmmPaU3m5g`Lw)5#@uFpQF|hyu literal 0 HcmV?d00001 diff --git a/frontend/pshared/lib/data/mapper/account/account.dart b/frontend/pshared/lib/data/mapper/account/account.dart new file mode 100644 index 0000000..fd13dca --- /dev/null +++ b/frontend/pshared/lib/data/mapper/account/account.dart @@ -0,0 +1,26 @@ +import 'package:pshared/data/dto/account/account.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/storable.dart'; + + +extension AccountMapper on Account { + AccountDTO toDTO() => AccountDTO( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + name: name, + avatarUrl: avatarUrl, + locale: locale, + login: login, + ); +} + +extension AccountDTOMapper on AccountDTO { + Account toDomain() => Account( + storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), + avatarUrl: avatarUrl, + locale: locale, + login: login, + name: name, + ); +} diff --git a/frontend/pshared/lib/data/mapper/account/base.dart b/frontend/pshared/lib/data/mapper/account/base.dart new file mode 100644 index 0000000..f0241ef --- /dev/null +++ b/frontend/pshared/lib/data/mapper/account/base.dart @@ -0,0 +1,24 @@ +import 'package:pshared/data/dto/account/base.dart'; +import 'package:pshared/models/account/base.dart'; +import 'package:pshared/models/storable.dart'; + + +extension AccountBaseMapper on AccountBase { + AccountBaseDTO toDTO() => AccountBaseDTO( + id: storable.id, + createdAt: storable.createdAt, + updatedAt: storable.updatedAt, + avatarUrl: avatarUrl, + name: name, + locale: locale, + ); +} + +extension AccountDTOMapper on AccountBaseDTO { + AccountBase toDomain() => AccountBase( + storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), + avatarUrl: avatarUrl, + name: name, + locale: locale, + ); +} diff --git a/frontend/pshared/lib/data/mapper/icon.dart b/frontend/pshared/lib/data/mapper/icon.dart new file mode 100644 index 0000000..a9319d1 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/icon.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; + + +final Map iconMapping = { + // General Project Management Icons + 'dashboard': Icons.dashboard, // Overview screen + 'project': Icons.work, // Represents a project + 'tasks': Icons.check_box, // Task list + 'calendar': Icons.calendar_today, // Calendar view + 'team': Icons.group, // Team collaboration + 'kanban': Icons.view_column, // Kanban board + 'timeline': Icons.timeline, // Project timeline + 'milestone': Icons.flag, // Milestones + 'priority': Icons.priority_high, // General priority indicator + 'settings': Icons.settings, // Settings or configurations + 'chat': Icons.chat, // Communication/chat + 'files': Icons.insert_drive_file, // File management + 'notes': Icons.note, // Notes or documentation + 'report': Icons.insert_chart, // Reporting and analytics + + // Priority Related Icons + 'to_do': Icons.assignment, // To do tasks + 'in_progress': Icons.autorenew, // Tasks in progress + 'complete': Icons.check_circle, // Completed tasks + 'archived': Icons.archive, // Archived tasks + + // Deadline Related Icons + 'deadline': Icons.timer, // Deadline indicator + 'reminder': Icons.alarm, // Deadline reminder + 'due_today': Icons.today, // Tasks due today + 'upcoming': Icons.event_available, // Upcoming deadlines + + // Additional + 'overdue': Icons.warning, // Overdue tasks or deadlines + 'budget': Icons.account_balance_wallet, // Budget and finance + 'resource': Icons.perm_contact_calendar, // Resource allocation + 'risk': Icons.warning_amber, // Risk management + 'feedback': Icons.feedback, // Feedback and reviews + 'timeline_edit': Icons.edit_calendar, // Edit project timeline + 'workflow': Icons.shuffle, // Workflow management + 'dependencies': Icons.link, // Task dependencies + 'progress': Icons.show_chart, // Project progress + 'schedule': Icons.schedule, // Scheduling + 'support': Icons.support, // Support/help + 'permissions': Icons.lock, // Access permissions + 'backup': Icons.backup, // Data backup + 'integration': Icons.extension, // Integrations + 'search': Icons.search, // Search functionality + 'announcement': Icons.announcement, // Announcements or updates + 'analytics': Icons.analytics, // Project analytics + 'assignment': Icons.assignment_turned_in, // Task assignments + 'discussions': Icons.forum, // Discussions/threads + 'timeline_view': Icons.view_timeline, // Detailed timeline view + 'board': Icons.dashboard_customize, // Custom project boards + 'approval': Icons.how_to_vote, // Approvals + 'review': Icons.rate_review, // Reviews and feedback + 'objective': Icons.golf_course, // Objectives/goals + 'settings_advanced': Icons.tune, // Advanced settings + 'time_tracking': Icons.timer_outlined, // Time tracking + 'checklist': Icons.checklist, // Checklists + 'sync': Icons.sync, // Syncing data + 'upload': Icons.cloud_upload, // Upload files + 'download': Icons.cloud_download, // Download files + 'share': Icons.share, // Sharing options + 'tag': Icons.label, // Tags/labels + 'notifications': Icons.notifications, // Notifications + 'user_roles': Icons.manage_accounts, // User roles and permissions + 'logout': Icons.logout, // Logout + 'automation': Icons.auto_awesome, // Automation + 'history': Icons.history, // Project history/logs + 'estimate': Icons.calculate, // Estimates and costing + 'quality': Icons.verified, // Quality assurance + 'strategy': Icons.lightbulb, // Strategy planning + 'feedback_form': Icons.comment, // Feedback forms + 'presentation': Icons.slideshow, // Project presentations +}; + + +class ProjectIcons { + static const IconData dashboard = Icons.dashboard; + static const IconData project = Icons.work; + static const IconData tasks = Icons.check_box; + static const IconData calendar = Icons.calendar_today; + static const IconData team = Icons.group; + static const IconData kanban = Icons.view_column; + static const IconData timeline = Icons.timeline; + static const IconData milestone = Icons.flag; + static const IconData priority = Icons.priority_high; + static const IconData settings = Icons.settings; + static const IconData chat = Icons.chat; + static const IconData files = Icons.insert_drive_file; + static const IconData notes = Icons.note; + static const IconData report = Icons.insert_chart; + static const IconData todo = Icons.assignment; + static const IconData inProgress = Icons.autorenew; + static const IconData complete = Icons.check_circle; + static const IconData archived = Icons.archive; + static const IconData deadline = Icons.timer; + static const IconData reminder = Icons.alarm; + static const IconData dueToday = Icons.today; + static const IconData upcoming = Icons.event_available; + static const IconData overdue = Icons.warning; + static const IconData budget = Icons.account_balance_wallet; + static const IconData resource = Icons.perm_contact_calendar; + static const IconData risk = Icons.warning_amber; + static const IconData feedback = Icons.feedback; + static const IconData timelineEdit = Icons.edit_calendar; + static const IconData workflow = Icons.shuffle; + static const IconData dependencies = Icons.link; + static const IconData progress = Icons.show_chart; + static const IconData schedule = Icons.schedule; + static const IconData support = Icons.support; + static const IconData permissions = Icons.lock; + static const IconData backup = Icons.backup; + static const IconData integration = Icons.extension; + static const IconData search = Icons.search; + static const IconData announcement = Icons.announcement; + static const IconData analytics = Icons.analytics; + static const IconData assignment = Icons.assignment_turned_in; + static const IconData discussions = Icons.forum; + static const IconData timelineView = Icons.view_timeline; + static const IconData board = Icons.dashboard_customize; + static const IconData approval = Icons.how_to_vote; + static const IconData review = Icons.rate_review; + static const IconData objective = Icons.golf_course; + static const IconData settingsAdvanced = Icons.tune; + static const IconData timeTracking = Icons.timer_outlined; + static const IconData checklist = Icons.checklist; + static const IconData sync = Icons.sync; + static const IconData upload = Icons.cloud_upload; + static const IconData download = Icons.cloud_download; + static const IconData share = Icons.share; + static const IconData tag = Icons.label; + static const IconData notifications = Icons.notifications; + static const IconData userRoles = Icons.manage_accounts; + static const IconData logout = Icons.logout; + static const IconData automation = Icons.auto_awesome; + static const IconData history = Icons.history; + static const IconData estimate = Icons.calculate; + static const IconData quality = Icons.verified; + static const IconData strategy = Icons.lightbulb; + static const IconData feedbackForm = Icons.comment; + static const IconData presentation = Icons.slideshow; +} + + +extension IconDataKeyExtension on IconData { + String get toIconKey { + return iconMapping.entries.firstWhere( + (entry) => entry.value == this, + orElse: () => throw Exception('IconData not found in mapping.'), + ).key; + } +} + +extension IconDataFromKeyExtension on String { + IconData get toIconData { + final iconData = iconMapping[this]; + if (iconData == null) { + throw Exception('No IconData found for key: $this'); + } + return iconData; + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/mapper/organization.dart b/frontend/pshared/lib/data/mapper/organization.dart new file mode 100644 index 0000000..41970d5 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/organization.dart @@ -0,0 +1,22 @@ +import 'package:pshared/data/dto/organization.dart'; +import 'package:pshared/models/organization/organization.dart'; +import 'package:pshared/models/storable.dart'; + + +extension OrganizationMapper on Organization { + OrganizationDTO toDTO() => OrganizationDTO( + id: storable.id, + createdAt: storable.createdAt, + updatedAt: storable.updatedAt, + timeZone: timeZone, + logoUrl: logoUrl, + ); +} + +extension OrganizationDTOMapper on OrganizationDTO { + Organization toDomain() => Organization( + storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), + timeZone: timeZone, + logoUrl: logoUrl, + ); +} diff --git a/frontend/pshared/lib/data/mapper/organization/description.dart b/frontend/pshared/lib/data/mapper/organization/description.dart new file mode 100644 index 0000000..d80483f --- /dev/null +++ b/frontend/pshared/lib/data/mapper/organization/description.dart @@ -0,0 +1,15 @@ +import 'package:pshared/data/dto/organization/description.dart'; +import 'package:pshared/models/organization/description.dart'; + + +extension OrganizationDescriptionMapper on OrganizationDescription { + OrganizationDescriptionDTO toDTO() => OrganizationDescriptionDTO( + logoUrl: logoUrl, + ); +} + +extension AccountDescriptionDTOMapper on OrganizationDescriptionDTO { + OrganizationDescription toDomain() => OrganizationDescription( + logoUrl: logoUrl, + ); +} diff --git a/frontend/pshared/lib/data/mapper/permissions/action_effect.dart b/frontend/pshared/lib/data/mapper/permissions/action_effect.dart new file mode 100644 index 0000000..9fa205f --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/action_effect.dart @@ -0,0 +1,23 @@ +import 'package:pshared/data/dto/permissions/action_effect.dart'; +import 'package:pshared/models/permissions/action_effect.dart'; +import 'package:pshared/models/permissions/action.dart'; +import 'package:pshared/models/permissions/effect.dart'; + + +extension ActionEffectMapper on ActionEffect { + ActionEffectDTO toDTO() { + return ActionEffectDTO( + action: action.toShortString(), + effect: effect.toShortString(), + ); + } +} + +extension ActionEffectDTOMapper on ActionEffectDTO { + ActionEffect toDomain() { + return ActionEffect( + action: ActionExtension.fromString(action), + effect: EffectExtension.fromString(effect), + ); + } +} diff --git a/frontend/pshared/lib/data/mapper/permissions/data/permission.dart b/frontend/pshared/lib/data/mapper/permissions/data/permission.dart new file mode 100644 index 0000000..82ebca6 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/data/permission.dart @@ -0,0 +1,33 @@ +import 'package:pshared/data/dto/permissions/data/permission.dart'; +import 'package:pshared/data/mapper/permissions/action_effect.dart'; +import 'package:pshared/models/permissions/data/permission.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; + + +extension PermissionMapper on Permission { + PermissionDTO toDTO() { + return PermissionDTO( + roleDescriptionRef: policy.roleDescriptionRef, + organizationRef: policy.organizationRef, + descriptionRef: policy.descriptionRef, + objectRef: policy.objectRef, + effect: policy.effect.toDTO(), + accountRef: accountRef, + ); + } +} + +extension PermissionDTOMapper on PermissionDTO { + Permission toDomain() { + return Permission( + policy: Policy( + roleDescriptionRef: roleDescriptionRef, + organizationRef: organizationRef, + descriptionRef: descriptionRef, + objectRef: objectRef, + effect: effect.toDomain(), + ), + accountRef: accountRef, + ); + } +} diff --git a/frontend/pshared/lib/data/mapper/permissions/data/permissions.dart b/frontend/pshared/lib/data/mapper/permissions/data/permissions.dart new file mode 100644 index 0000000..4d71717 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/data/permissions.dart @@ -0,0 +1,26 @@ +import 'package:pshared/data/dto/permissions/data/permissions.dart'; +import 'package:pshared/data/mapper/permissions/data/permission.dart'; +import 'package:pshared/data/mapper/permissions/data/policy.dart'; +import 'package:pshared/data/mapper/permissions/data/role.dart'; +import 'package:pshared/models/permissions/data/permissions.dart'; + + +extension PermissionsDataMapper on PermissionsData { + PermissionsDataDTO toDTO() { + return PermissionsDataDTO( + roles: roles.map((role) => role.toDTO()).toList(), + policies: policies.map((policy) => policy.toDTO()).toList(), + permissions: permissions.map((permission) => permission.toDTO()).toList(), + ); + } +} + +extension PermissionsDataDTOMapper on PermissionsDataDTO { + PermissionsData toDomain() { + return PermissionsData( + roles: roles.map((role) => role.toDomain()).toList(), + policies: policies.map((policy) => policy.toDomain()).toList(), + permissions: permissions.map((permission) => permission.toDomain()).toList(), + ); + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/mapper/permissions/data/policy.dart b/frontend/pshared/lib/data/mapper/permissions/data/policy.dart new file mode 100644 index 0000000..70de889 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/data/policy.dart @@ -0,0 +1,28 @@ +import 'package:pshared/data/dto/permissions/data/policy.dart'; +import 'package:pshared/data/mapper/permissions/action_effect.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; + + +extension PolicyMapper on Policy { + PolicyDTO toDTO() { + return PolicyDTO( + roleDescriptionRef: roleDescriptionRef, + organizationRef: organizationRef, + descriptionRef: descriptionRef, + objectRef: objectRef, + effect: effect.toDTO(), + ); + } +} + +extension PolicyDTOMapper on PolicyDTO { + Policy toDomain() { + return Policy( + roleDescriptionRef: roleDescriptionRef, + organizationRef: organizationRef, + descriptionRef: descriptionRef, + objectRef: objectRef, + effect: effect.toDomain(), + ); + } +} diff --git a/frontend/pshared/lib/data/mapper/permissions/data/role.dart b/frontend/pshared/lib/data/mapper/permissions/data/role.dart new file mode 100644 index 0000000..de959da --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/data/role.dart @@ -0,0 +1,25 @@ +import 'package:pshared/data/dto/permissions/data/role.dart'; +import 'package:pshared/models/permissions/data/role.dart'; + + +extension RoleMapper on Role { + /// Converts a `Role` domain model to a `RoleDTO`. + RoleDTO toDTO() { + return RoleDTO( + accountRef: accountRef, + descriptionRef: descriptionRef, + organizationRef: organizationRef, + ); + } +} + +extension RoleDTOMapper on RoleDTO { + /// Converts a `RoleDTO` to a `Role` domain model. + Role toDomain() { + return Role( + accountRef: accountRef, + descriptionRef: descriptionRef, + organizationRef: organizationRef, + ); + } +} diff --git a/frontend/pshared/lib/data/mapper/permissions/descriptions/description.dart b/frontend/pshared/lib/data/mapper/permissions/descriptions/description.dart new file mode 100644 index 0000000..4d8ac22 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/descriptions/description.dart @@ -0,0 +1,23 @@ +import 'package:pshared/data/dto/permissions/description/description.dart'; +import 'package:pshared/data/mapper/permissions/descriptions/policy.dart'; +import 'package:pshared/data/mapper/permissions/descriptions/role.dart'; +import 'package:pshared/models/permissions/descriptions/permissions.dart'; + + +extension PermissionsDescriptionMapper on PermissionsDescription { + PermissionsDescriptionDTO toDTO() { + return PermissionsDescriptionDTO( + roles: roles.map((role) => role.toDTO()).toList(), + policies: policies.map((policy) => policy.toDTO()).toList(), + ); + } +} + +extension PermissionsDescriptionDTOMapper on PermissionsDescriptionDTO { + PermissionsDescription toDomain() { + return PermissionsDescription( + roles: roles.map((role) => role.toDomain()).toList(), + policies: policies.map((policy) => policy.toDomain()).toList(), + ); + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart b/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart new file mode 100644 index 0000000..e444a9d --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/descriptions/policy.dart @@ -0,0 +1,22 @@ +import 'package:pshared/data/dto/permissions/description/policy.dart'; +import 'package:pshared/models/permissions/descriptions/policy.dart'; +import 'package:pshared/models/storable.dart'; + + +extension PolicyDescriptionMapper on PolicyDescription { + PolicyDescriptionDTO toDTO() => PolicyDescriptionDTO( + id: storable.id, + createdAt: storable.createdAt, + updatedAt: storable.updatedAt, + resourceTypes: resourceTypes, + organizationRef: organizationRef, + ); +} + +extension PolicyDescriptionDTOMapper on PolicyDescriptionDTO { + PolicyDescription toDomain() => PolicyDescription( + storable: newStorable(id: id, createdAt: createdAt, updatedAt: createdAt), + resourceTypes: resourceTypes, + organizationRef: organizationRef, + ); +} diff --git a/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart b/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart new file mode 100644 index 0000000..d0596cb --- /dev/null +++ b/frontend/pshared/lib/data/mapper/permissions/descriptions/role.dart @@ -0,0 +1,20 @@ +import 'package:pshared/data/dto/permissions/description/role.dart'; +import 'package:pshared/models/permissions/descriptions/role.dart'; +import 'package:pshared/models/storable.dart'; + + +extension RoleDescriptionMapper on RoleDescription { + RoleDescriptionDTO toDTO() => RoleDescriptionDTO( + id: storable.id, + createdAt: storable.createdAt, + updatedAt: storable.updatedAt, + organizationRef: organizationRef, + ); +} + +extension RoleDescriptionDTOMapper on RoleDescriptionDTO { + RoleDescription toDomain() => RoleDescription( + storable: newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt), + organizationRef: organizationRef, + ); +} diff --git a/frontend/pshared/lib/data/mapper/storable.dart b/frontend/pshared/lib/data/mapper/storable.dart new file mode 100644 index 0000000..8990947 --- /dev/null +++ b/frontend/pshared/lib/data/mapper/storable.dart @@ -0,0 +1,11 @@ +import 'package:pshared/data/dto/storable.dart'; +import 'package:pshared/models/storable.dart'; + + +extension StorableMapper on Storable { + StorableDTO toDTO() => StorableDTO(id: id, createdAt: createdAt, updatedAt: updatedAt); +} + +extension StorableDTOMapper on StorableDTO { + Storable toDomain() => newStorable(id: id, createdAt: createdAt, updatedAt: updatedAt); +} diff --git a/frontend/pshared/lib/l10n/ps_en.arb b/frontend/pshared/lib/l10n/ps_en.arb new file mode 100644 index 0000000..53fa531 --- /dev/null +++ b/frontend/pshared/lib/l10n/ps_en.arb @@ -0,0 +1,23 @@ +{ + "@@locale": "en", + + "statusReady": "Ready", + "statusRegistered": "Registered", + "statusNotRegistered": "Not Registered", + "typeInternal": "Internal", + "typeExternal": "External", + "operationStatusProcessing": "Processing", + "@operationStatusProcessing": { + "description": "Label for the “processing” operation status" + }, + + "operationStatusSuccess": "Success", + "@operationStatusSuccess": { + "description": "Label for the “success” operation status" + }, + + "operationStatusError": "Error", + "@operationStatusError": { + "description": "Label for the “error” operation status" + } +} diff --git a/frontend/pshared/lib/models/account/account.dart b/frontend/pshared/lib/models/account/account.dart new file mode 100644 index 0000000..2d41b0a --- /dev/null +++ b/frontend/pshared/lib/models/account/account.dart @@ -0,0 +1,36 @@ +import 'package:pshared/models/account/base.dart'; + + +class Account extends AccountBase { + final String login; + + const Account({ + required super.storable, + required super.avatarUrl, + required this.login, + required super.locale, + required super.name, + }); + + factory Account.fromBase(AccountBase accountBase, String login) => Account( + storable: accountBase.storable, + avatarUrl: accountBase.avatarUrl, + locale: accountBase.locale, + name: accountBase.name, + login: login, + ); + + @override + Account copyWith({ + String? Function()? avatarUrl, + String? name, + String? locale, + }) { + final updatedBase = super.copyWith( + avatarUrl: avatarUrl, + name: name, + locale: locale, + ); + return Account.fromBase(updatedBase, login); + } +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/account/base.dart b/frontend/pshared/lib/models/account/base.dart new file mode 100644 index 0000000..5fdd112 --- /dev/null +++ b/frontend/pshared/lib/models/account/base.dart @@ -0,0 +1,35 @@ +import 'package:pshared/models/storable.dart'; + + +class AccountBase implements Storable { + final Storable storable; + + @override + String get id => storable.id; + @override + DateTime get createdAt => storable.createdAt; + @override + DateTime get updatedAt => storable.updatedAt; + + final String? avatarUrl; + final String name; + final String locale; + + const AccountBase({ + required this.storable, + required this.name, + required this.locale, + required this.avatarUrl, + }); + + AccountBase copyWith({ + String? Function()? avatarUrl, + String? name, + String? locale, + }) => AccountBase( + storable: storable, + avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl, + locale: locale ?? this.locale, + name: name ?? this.name, + ); +} diff --git a/frontend/pshared/lib/models/organization/description.dart b/frontend/pshared/lib/models/organization/description.dart new file mode 100644 index 0000000..7e6f5f9 --- /dev/null +++ b/frontend/pshared/lib/models/organization/description.dart @@ -0,0 +1,7 @@ +class OrganizationDescription { + final String? logoUrl; + + const OrganizationDescription({ + this.logoUrl, + }); +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/organization/employee.dart b/frontend/pshared/lib/models/organization/employee.dart new file mode 100644 index 0000000..1124596 --- /dev/null +++ b/frontend/pshared/lib/models/organization/employee.dart @@ -0,0 +1,4 @@ +import 'package:pshared/models/account/account.dart'; + + +typedef Employee = Account; \ No newline at end of file diff --git a/frontend/pshared/lib/models/organization/organization.dart b/frontend/pshared/lib/models/organization/organization.dart new file mode 100644 index 0000000..b04182a --- /dev/null +++ b/frontend/pshared/lib/models/organization/organization.dart @@ -0,0 +1,34 @@ +import 'package:pshared/models/storable.dart'; + + +class Organization implements Storable { + final Storable storable; + + @override + String get id => storable.id; + @override + DateTime get createdAt => storable.createdAt; + @override + DateTime get updatedAt => storable.updatedAt; + + final String timeZone; + final String? logoUrl; + + const Organization({ + required this.storable, + required this.timeZone, + this.logoUrl, + }); + + + Organization copyWith({ + String? name, + String? Function()? description, + String? timeZone, + String? Function()? logoUrl, + }) => Organization( + storable: storable, // Same Storable, same id + timeZone: timeZone ?? this.timeZone, + logoUrl: logoUrl != null ? logoUrl() : this.logoUrl, + ); +} diff --git a/frontend/pshared/lib/models/payment/methods/card.dart b/frontend/pshared/lib/models/payment/methods/card.dart new file mode 100644 index 0000000..6643e2b --- /dev/null +++ b/frontend/pshared/lib/models/payment/methods/card.dart @@ -0,0 +1,18 @@ +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; + + +class CardPaymentMethod extends PaymentMethodData { + @override + final PaymentType type = PaymentType.card; + + final String pan; + final String firstName; + final String lastName; + + CardPaymentMethod({ + required this.pan, + required this.firstName, + required this.lastName, + }); +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/payment/methods/data.dart b/frontend/pshared/lib/models/payment/methods/data.dart new file mode 100644 index 0000000..78780bb --- /dev/null +++ b/frontend/pshared/lib/models/payment/methods/data.dart @@ -0,0 +1,6 @@ +import 'package:pshared/models/payment/type.dart'; + + +abstract class PaymentMethodData { + PaymentType get type; +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/payment/methods/iban.dart b/frontend/pshared/lib/models/payment/methods/iban.dart new file mode 100644 index 0000000..83f7698 --- /dev/null +++ b/frontend/pshared/lib/models/payment/methods/iban.dart @@ -0,0 +1,20 @@ +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; + + +class IbanPaymentMethod extends PaymentMethodData { + @override + final PaymentType type = PaymentType.iban; + + final String iban; // e.g. DE89 3704 0044 0532 0130 00 + final String accountHolder; // Full name of the recipient + final String? bic; // Optional: for cross-border transfers + final String? bankName; // Optional: for UI clarity + + IbanPaymentMethod({ + required this.iban, + required this.accountHolder, + this.bic, + this.bankName, + }); +} diff --git a/frontend/pshared/lib/models/payment/methods/russian_bank.dart b/frontend/pshared/lib/models/payment/methods/russian_bank.dart new file mode 100644 index 0000000..70de691 --- /dev/null +++ b/frontend/pshared/lib/models/payment/methods/russian_bank.dart @@ -0,0 +1,26 @@ +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; + + +class RussianBankAccountPaymentMethod extends PaymentMethodData { + @override + final PaymentType type = PaymentType.bankAccount; + + final String recipientName; + final String inn; + final String kpp; + final String bankName; + final String bik; + final String accountNumber; + final String correspondentAccount; + + RussianBankAccountPaymentMethod({ + required this.recipientName, + required this.inn, + required this.kpp, + required this.bankName, + required this.bik, + required this.accountNumber, + required this.correspondentAccount, + }); +} diff --git a/frontend/pshared/lib/models/payment/methods/type.dart b/frontend/pshared/lib/models/payment/methods/type.dart new file mode 100644 index 0000000..cb55fc8 --- /dev/null +++ b/frontend/pshared/lib/models/payment/methods/type.dart @@ -0,0 +1,21 @@ +import 'package:pshared/models/payment/type.dart'; + + +class PaymentMethod { + PaymentMethod({ + required this.id, + required this.label, + required this.details, + required this.type, + this.isEnabled = true, + this.isMain = false, + }); + + final String id; + final String label; + final String details; + final PaymentType type; + + bool isEnabled; + bool isMain; +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/payment/methods/wallet.dart b/frontend/pshared/lib/models/payment/methods/wallet.dart new file mode 100644 index 0000000..587631e --- /dev/null +++ b/frontend/pshared/lib/models/payment/methods/wallet.dart @@ -0,0 +1,12 @@ +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; + + +class WalletPaymentMethod extends PaymentMethodData { + @override + final PaymentType type = PaymentType.wallet; + + final String walletId; + + WalletPaymentMethod({required this.walletId}); +} diff --git a/frontend/pshared/lib/models/payment/operation.dart b/frontend/pshared/lib/models/payment/operation.dart new file mode 100644 index 0000000..40ed8b6 --- /dev/null +++ b/frontend/pshared/lib/models/payment/operation.dart @@ -0,0 +1,30 @@ +import 'package:pshared/models/payment/status.dart'; + + +class OperationItem { + final OperationStatus status; + final String? fileName; + final double amount; + final String currency; + final double toAmount; + final String toCurrency; + final String payId; + final String? cardNumber; + final String name; + final DateTime date; + final String comment; + + OperationItem({ + required this.status, + this.fileName, + required this.amount, + required this.currency, + required this.toAmount, + required this.toCurrency, + required this.payId, + this.cardNumber, + required this.name, + required this.date, + required this.comment, + }); +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/payment/status.dart b/frontend/pshared/lib/models/payment/status.dart new file mode 100644 index 0000000..68acfda --- /dev/null +++ b/frontend/pshared/lib/models/payment/status.dart @@ -0,0 +1,25 @@ +import 'package:flutter/widgets.dart'; +import 'package:pshared/generated/i18n/ps_localizations.dart'; + +enum OperationStatus { + processing, + success, + error, +} + + +extension OperationStatusX on OperationStatus { + /// Returns the localized string for this status, + /// e.g. “Processing”, “Success”, “Error”. + String localized(BuildContext context) { + final loc = PSLocalizations.of(context)!; + switch (this) { + case OperationStatus.processing: + return loc.operationStatusProcessing; + case OperationStatus.success: + return loc.operationStatusSuccess; + case OperationStatus.error: + return loc.operationStatusError; + } + } +} diff --git a/frontend/pshared/lib/models/payment/type.dart b/frontend/pshared/lib/models/payment/type.dart new file mode 100644 index 0000000..0dd75d6 --- /dev/null +++ b/frontend/pshared/lib/models/payment/type.dart @@ -0,0 +1,6 @@ +enum PaymentType { + bankAccount, + iban, + wallet, + card, +} diff --git a/frontend/pshared/lib/models/payment/upload_history_item.dart b/frontend/pshared/lib/models/payment/upload_history_item.dart new file mode 100644 index 0000000..3c131d8 --- /dev/null +++ b/frontend/pshared/lib/models/payment/upload_history_item.dart @@ -0,0 +1,11 @@ +class UploadHistoryItem { + final String name; + final String status; + final String time; + + UploadHistoryItem({ + required this.name, + required this.status, + required this.time, + }); +} diff --git a/frontend/pshared/lib/models/permission_bound.dart b/frontend/pshared/lib/models/permission_bound.dart new file mode 100644 index 0000000..6618f26 --- /dev/null +++ b/frontend/pshared/lib/models/permission_bound.dart @@ -0,0 +1,22 @@ +import 'package:pshared/config/constants.dart'; + + +abstract class PermissionBound { + String get permissionRef; + String get organizationRef; +} + +class _PermissionBoundImp implements PermissionBound { + @override + final String permissionRef; + @override + final String organizationRef; + + const _PermissionBoundImp({ + required this.permissionRef, + required this.organizationRef, + }); +} + +PermissionBound newPermissionBound({ required String organizationRef, String? permissionRef}) => + _PermissionBoundImp(permissionRef: permissionRef ?? Constants.nilObjectRef, organizationRef: organizationRef); \ No newline at end of file diff --git a/frontend/pshared/lib/models/permission_bound_storable.dart b/frontend/pshared/lib/models/permission_bound_storable.dart new file mode 100644 index 0000000..4ee0d63 --- /dev/null +++ b/frontend/pshared/lib/models/permission_bound_storable.dart @@ -0,0 +1,6 @@ +import 'package:pshared/models/permission_bound.dart'; +import 'package:pshared/models/storable.dart'; + + +abstract class PermissionBoundStorable implements PermissionBound, Storable { +} diff --git a/frontend/pshared/lib/models/permissions/access.dart b/frontend/pshared/lib/models/permissions/access.dart new file mode 100644 index 0000000..4bf5031 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/access.dart @@ -0,0 +1,13 @@ +import 'package:pshared/models/permissions/data/permissions.dart'; +import 'package:pshared/models/permissions/descriptions/permissions.dart'; + + +class UserAccess { + final PermissionsDescription descriptions; + final PermissionsData permissions; + + const UserAccess({ + required this.descriptions, + required this.permissions, + }); +} diff --git a/frontend/pshared/lib/models/permissions/action.dart b/frontend/pshared/lib/models/permissions/action.dart new file mode 100644 index 0000000..1d99a6a --- /dev/null +++ b/frontend/pshared/lib/models/permissions/action.dart @@ -0,0 +1,15 @@ +enum Action { + create, + read, + update, + delete, +} + +extension ActionExtension on Action { + String toShortString() => toString().split('.').last; + + static Action fromString(String value) => Action.values.firstWhere( + (e) => e.toShortString() == value, + orElse: () => throw ArgumentError('Invalid action: $value'), + ); +} diff --git a/frontend/pshared/lib/models/permissions/action_effect.dart b/frontend/pshared/lib/models/permissions/action_effect.dart new file mode 100644 index 0000000..b6be501 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/action_effect.dart @@ -0,0 +1,13 @@ +import 'package:pshared/models/permissions/action.dart'; +import 'package:pshared/models/permissions/effect.dart'; + + +class ActionEffect { + final Action action; // The action allowed or denied + final Effect effect; // The effect of the policy ("allow" or "deny") + + const ActionEffect({ + required this.action, + required this.effect, + }); +} diff --git a/frontend/pshared/lib/models/permissions/data/permission.dart b/frontend/pshared/lib/models/permissions/data/permission.dart new file mode 100644 index 0000000..fc8c4d8 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/data/permission.dart @@ -0,0 +1,12 @@ +import 'package:pshared/models/permissions/data/policy.dart'; + + +class Permission { + final Policy policy; + final String accountRef; + + const Permission({ + required this.policy, + required this.accountRef, + }); +} diff --git a/frontend/pshared/lib/models/permissions/data/permissions.dart b/frontend/pshared/lib/models/permissions/data/permissions.dart new file mode 100644 index 0000000..ef97108 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/data/permissions.dart @@ -0,0 +1,16 @@ +import 'package:pshared/models/permissions/data/permission.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; +import 'package:pshared/models/permissions/data/role.dart'; + + +class PermissionsData { + final List roles; + final List policies; + final List permissions; + + const PermissionsData({ + required this.roles, + required this.policies, + required this.permissions, + }); +} diff --git a/frontend/pshared/lib/models/permissions/data/policy.dart b/frontend/pshared/lib/models/permissions/data/policy.dart new file mode 100644 index 0000000..30c0ec6 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/data/policy.dart @@ -0,0 +1,18 @@ +import 'package:pshared/models/permissions/action_effect.dart'; + + +class Policy { + final String roleDescriptionRef; + final String organizationRef; + final String descriptionRef; + final String? objectRef; + final ActionEffect effect; + + const Policy({ + required this.roleDescriptionRef, + required this.organizationRef, + required this.descriptionRef, + required this.objectRef, + required this.effect, + }); +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/permissions/data/role.dart b/frontend/pshared/lib/models/permissions/data/role.dart new file mode 100644 index 0000000..536ef17 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/data/role.dart @@ -0,0 +1,11 @@ +class Role { + final String accountRef; + final String organizationRef; + final String descriptionRef; + + const Role({ + required this.accountRef, + required this.descriptionRef, + required this.organizationRef, + }); +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/permissions/descriptions/permissions.dart b/frontend/pshared/lib/models/permissions/descriptions/permissions.dart new file mode 100644 index 0000000..5b08635 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/descriptions/permissions.dart @@ -0,0 +1,13 @@ +import 'package:pshared/models/permissions/descriptions/policy.dart'; +import 'package:pshared/models/permissions/descriptions/role.dart'; + + +class PermissionsDescription { + final List roles; + final List policies; + + const PermissionsDescription({ + required this.roles, + required this.policies, + }); +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/permissions/descriptions/policy.dart b/frontend/pshared/lib/models/permissions/descriptions/policy.dart new file mode 100644 index 0000000..6aecef0 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/descriptions/policy.dart @@ -0,0 +1,22 @@ +import 'package:pshared/models/resources.dart'; +import 'package:pshared/models/storable.dart'; + + +class PolicyDescription implements Storable { + final Storable storable; + final List? resourceTypes; + final String? organizationRef; + + @override + String get id => storable.id; + @override + DateTime get createdAt => storable.createdAt; + @override + DateTime get updatedAt => storable.updatedAt; + + const PolicyDescription({ + required this.storable, + required this.resourceTypes, + required this.organizationRef, + }); +} \ No newline at end of file diff --git a/frontend/pshared/lib/models/permissions/descriptions/role.dart b/frontend/pshared/lib/models/permissions/descriptions/role.dart new file mode 100644 index 0000000..ecde4fb --- /dev/null +++ b/frontend/pshared/lib/models/permissions/descriptions/role.dart @@ -0,0 +1,27 @@ +import 'package:pshared/models/storable.dart'; + + +class RoleDescription implements Storable { + final Storable storable; + + @override + String get id => storable.id; + @override + DateTime get createdAt => storable.createdAt; + @override + DateTime get updatedAt => storable.updatedAt; + + final String organizationRef; + + const RoleDescription({ + required this.storable, + required this.organizationRef, + }); + + factory RoleDescription.build({ + required String organizationRef, + }) => RoleDescription( + storable: newStorable(), + organizationRef: organizationRef + ); +} diff --git a/frontend/pshared/lib/models/permissions/effect.dart b/frontend/pshared/lib/models/permissions/effect.dart new file mode 100644 index 0000000..8019772 --- /dev/null +++ b/frontend/pshared/lib/models/permissions/effect.dart @@ -0,0 +1,13 @@ +enum Effect { + allow, + deny, +} + +extension EffectExtension on Effect { + String toShortString() => toString().split('.').last; + + static Effect fromString(String value) => Effect.values.firstWhere( + (e) => e.toShortString() == value, + orElse: () => throw ArgumentError('Invalid effect: $value'), + ); +} diff --git a/frontend/pshared/lib/models/pfe/services.dart b/frontend/pshared/lib/models/pfe/services.dart new file mode 100644 index 0000000..e69de29 diff --git a/frontend/pshared/lib/models/recipient/filter.dart b/frontend/pshared/lib/models/recipient/filter.dart new file mode 100644 index 0000000..52d3b0f --- /dev/null +++ b/frontend/pshared/lib/models/recipient/filter.dart @@ -0,0 +1 @@ +enum RecipientFilter { all, ready, registered, notRegistered } \ No newline at end of file diff --git a/frontend/pshared/lib/models/recipient/recipient.dart b/frontend/pshared/lib/models/recipient/recipient.dart new file mode 100644 index 0000000..f131920 --- /dev/null +++ b/frontend/pshared/lib/models/recipient/recipient.dart @@ -0,0 +1,78 @@ +import 'package:pshared/models/payment/methods/card.dart'; +import 'package:pshared/models/payment/methods/iban.dart'; +import 'package:pshared/models/payment/methods/russian_bank.dart'; +import 'package:pshared/models/payment/methods/wallet.dart'; +import 'package:pshared/models/recipient/status.dart'; +import 'package:pshared/models/recipient/type.dart'; + + +class Recipient { + final String? avatarUrl; // network URL / local asset + final String name; + final String email; + final RecipientStatus status; + final RecipientType type; + final CardPaymentMethod? card; + final IbanPaymentMethod? iban; + final RussianBankAccountPaymentMethod? bank; + final WalletPaymentMethod? wallet; + + const Recipient({ + this.avatarUrl, + required this.name, + required this.email, + required this.status, + required this.type, + this.card, + this.iban, + this.bank, + this.wallet, + }); + + /// Convenience factory for quickly creating mock recipients. + factory Recipient.mock({ + required String name, + required String email, + required RecipientStatus status, + required RecipientType type, + CardPaymentMethod? card, + IbanPaymentMethod? iban, + RussianBankAccountPaymentMethod? bank, + WalletPaymentMethod? wallet, + }) => + Recipient( + avatarUrl: null, + name: name, + email: email, + status: status, + type: type, + card: card, + iban: iban, + bank: bank, + wallet: wallet, + ); + + bool matchesQuery(String q) { + final searchable = [ + name, + email, + card?.pan, + card?.firstName, + card?.lastName, + iban?.iban, + iban?.accountHolder, + iban?.bic, + iban?.bankName, + bank?.accountNumber, + bank?.recipientName, + bank?.inn, + bank?.kpp, + bank?.bankName, + bank?.bik, + bank?.correspondentAccount, + wallet?.walletId, + ]; + + return searchable.any((field) => field?.toLowerCase().contains(q) ?? false); + } +} diff --git a/frontend/pshared/lib/models/recipient/status.dart b/frontend/pshared/lib/models/recipient/status.dart new file mode 100644 index 0000000..ffbf681 --- /dev/null +++ b/frontend/pshared/lib/models/recipient/status.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; + +import 'package:pshared/generated/i18n/ps_localizations.dart'; + + +/// Possible payout readiness states. +enum RecipientStatus { ready, registered, notRegistered } + +extension RecipientStatusExtension on RecipientStatus { + /// Human-readable, **localized** label for display in the UI. + String label(BuildContext context) { + final l10n = PSLocalizations.of(context)!; + switch (this) { + case RecipientStatus.ready: + return l10n.statusReady; + case RecipientStatus.registered: + return l10n.statusRegistered; + case RecipientStatus.notRegistered: + return l10n.statusNotRegistered; + } + } +} diff --git a/frontend/pshared/lib/models/recipient/type.dart b/frontend/pshared/lib/models/recipient/type.dart new file mode 100644 index 0000000..e71c86b --- /dev/null +++ b/frontend/pshared/lib/models/recipient/type.dart @@ -0,0 +1,15 @@ +import 'package:flutter/widgets.dart'; + +import 'package:pshared/generated/i18n/ps_localizations.dart'; + + +/// Indicates whether you (internal) or the other party (external) manage payout data. +enum RecipientType { internal, external } + +extension RecipientTypeExtension on RecipientType { + /// Localized label – no opaque abbreviations. + String label(BuildContext context) => + this == RecipientType.internal + ? PSLocalizations.of(context)!.typeInternal + : PSLocalizations.of(context)!.typeExternal; +} diff --git a/frontend/pshared/lib/models/resources.dart b/frontend/pshared/lib/models/resources.dart new file mode 100644 index 0000000..851f2d6 --- /dev/null +++ b/frontend/pshared/lib/models/resources.dart @@ -0,0 +1,107 @@ +import 'package:json_annotation/json_annotation.dart'; + +/// Represents various resource types (mirroring your Go "Type" constants). +enum ResourceType { + /// Represents user accounts in the system + @JsonValue('accounts') + accounts, + + /// Represents analytics integration with Amplitude + @JsonValue('amplitude') + amplitude, + + /// Represents automation workflows + @JsonValue('automations') + automations, + + /// Tracks changes made to resources + @JsonValue('changes') + changes, + + /// Represents client information + @JsonValue('clients') + clients, + + /// Represents comments on tasks or other resources + @JsonValue('comments') + comments, + + /// Represents invitations sent to users + @JsonValue('invitations') + invitations, + + /// Represents invoices + @JsonValue('invoices') + invoices, + + /// Represents logos for organizations or projects + @JsonValue('logo') + logo, + + /// Represents notifications sent to users + @JsonValue('notifications') + notifications, + + /// Represents organizations in the system + @JsonValue('organizations') + organizations, + + /// Represents permissions service + @JsonValue('permissions') + permissions, + + /// Represents access control policies + @JsonValue('policies') + policies, + + /// Represents task or project priorities + @JsonValue('priorities') + priorities, + + /// Represents priority groups + @JsonValue('priority_groups') + priorityGroups, + + /// Represents projects managed in the system + @JsonValue('projects') + projects, + + @JsonValue('properties') + properties, + + /// Represents reactions + @JsonValue('reactions') + reactions, + + /// Represents refresh tokens for authentication + @JsonValue('refresh_tokens') + refreshTokens, + + /// Represents roles in access control + @JsonValue('roles') + roles, + + /// Represents statuses of tasks or projects + @JsonValue('statuses') + statuses, + + /// Represents steps in workflows or processes + @JsonValue('steps') + steps, + + /// Represents tasks managed in the system + @JsonValue('tasks') + tasks, + + /// Represents teams managed in the system + @JsonValue('teams') + teams, + + /// Represents workflows for tasks or projects + @JsonValue('workflows') + workflows, + + /// Represents workspaces containing projects and teams + @JsonValue('workspaces') + workspaces; +} diff --git a/frontend/pshared/lib/models/settings/localizations.dart b/frontend/pshared/lib/models/settings/localizations.dart new file mode 100644 index 0000000..f85fc64 --- /dev/null +++ b/frontend/pshared/lib/models/settings/localizations.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + + +typedef LangLocalization = Map; + +String _translation(LangLocalization loc, String key) { + return loc[key] ?? ''; +} + +typedef Localizations = Map; + +typedef Localizer = String Function(BuildContext); + +class Localization { + + static const String keyHint = 'hint'; + static const String keyLink = 'link'; + static const String keyName = 'name'; + static const String keyError = 'error'; + static const String keyAddress = 'address'; + static const String keyDetails = 'details'; + static const String keyRoute = 'route'; + static const String keyLocationName = 'location_name'; + + static String _localizeImp(Localizations localizations, String locale, String Function(LangLocalization) functor) { + final localization = localizations[locale]; + if (localization != null) { + return functor(localization); + } + return ''; + } + + static String _localize(Localizations localizations, String locale, String Function(LangLocalization) functor, {String? fallback}) { + final res = _localizeImp(localizations, locale, functor); + return res.isNotEmpty ? res : (fallback ?? ''); + } + + static bool localizationExists(Localizations loc, String locale) { + return loc.containsKey(locale); + } + + static String hint(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyHint), fallback: fallback); + } + + static String link(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyLink), fallback: fallback); + } + + static String name(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyName), fallback: fallback); + } + + static String error(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyError), fallback: fallback); + } + + static String address(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyAddress), fallback: fallback); + } + + static String details(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyDetails), fallback: fallback); + } + + static String route(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyRoute), fallback: fallback); + } + + static String locationName(Localizations loc, String locale, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, keyLocationName), fallback: fallback); + } + + static String translate(Localizations loc, String locale, String key, {String? fallback}) { + return _localize(loc, locale, (localization) => _translation(localization, key), fallback: fallback); + } +} diff --git a/frontend/pshared/lib/models/settings/time_validity.dart b/frontend/pshared/lib/models/settings/time_validity.dart new file mode 100644 index 0000000..ebd0e31 --- /dev/null +++ b/frontend/pshared/lib/models/settings/time_validity.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'time_validity.g.dart'; + + +@JsonSerializable() +class TimeValidity { + final DateTime? start; + final DateTime? expiry; + + const TimeValidity({this.start, this.expiry}); + + bool get isExpired => expiry?.isBefore(DateTime.now()) ?? false; + bool get isNotStarted => start?.isAfter(DateTime.now()) ?? false; + bool get isActive => (!isNotStarted) && (!isExpired); + + TimeValidity copyWith({ + DateTime? Function()? start, + DateTime? Function()? expiry, + }) => TimeValidity( + start: start == null ? this.start : start(), + expiry: expiry == null ? this.expiry : expiry(), + ); + + factory TimeValidity.fromJson(Map json) => _$TimeValidityFromJson(json); + Map toJson() => _$TimeValidityToJson(this); +} diff --git a/frontend/pshared/lib/models/storable.dart b/frontend/pshared/lib/models/storable.dart new file mode 100644 index 0000000..ce95482 --- /dev/null +++ b/frontend/pshared/lib/models/storable.dart @@ -0,0 +1,30 @@ +import 'package:pshared/config/constants.dart'; + + +abstract class Storable { + String get id; + DateTime get createdAt; + DateTime get updatedAt; +} + +class _StorableImp implements Storable { + @override + final String id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + + const _StorableImp({ + required this.id, + required this.createdAt, + required this.updatedAt, + }); + +} + +Storable newStorable({String? id, DateTime? createdAt, DateTime? updatedAt}) => _StorableImp( + id: id ?? Constants.nilObjectRef, + createdAt: createdAt ?? DateTime.now().toUtc(), + updatedAt: updatedAt ?? DateTime.now().toUtc(), +); diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart new file mode 100644 index 0000000..111ae4b --- /dev/null +++ b/frontend/pshared/lib/provider/account.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; + +import 'package:share_plus/share_plus.dart'; + +import 'package:pshared/api/errors/unauthorized.dart'; +import 'package:pshared/api/requests/signup.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/provider/exception.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/account.dart'; + + +class AccountProvider extends ChangeNotifier { + // The resource now wraps our Account? state along with its loading/error state. + Resource _resource = Resource(data: null); + Resource get resource => _resource; + + Account? get account => _resource.data; + bool get isLoggedIn => account != null; + bool get isLoading => _resource.isLoading; + Object? get error => _resource.error; + + // Private helper to update the resource and notify listeners. + void _setResource(Resource newResource) { + _resource = newResource; + notifyListeners(); + } + + + Future login({ + required String email, + required String password, + required String locale, + }) async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final acc = await AccountService.login(email, password, locale); + _setResource(Resource(data: acc, isLoading: false)); + return acc; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future restore() async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final acc = await AccountService.restore(); + _setResource(Resource(data: acc, isLoading: false)); + return acc; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future signup( + String name, + String login, + String password, + String locale, + String organizationName, + String timezone, + ) async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + await AccountService.signup( + SignupRequest.build( + name: name, + login: login.trim().toLowerCase(), + password: password, + locale: locale, + organizationName: organizationName, + organizationTimeZone: timezone, + ), + ); + // Signup might not automatically log in the user, + // so we just mark the request as complete. + _setResource(_resource.copyWith(isLoading: false)); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future logout() async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + await AccountService.logout(); + _setResource(Resource(data: null, isLoading: false)); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future update({ + String? locale, + String? avatarUrl, + String? notificationFrequency, + }) async { + if (account == null) throw ErrorUnauthorized(); + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final updated = await AccountService.update( + account!.copyWith( + avatarUrl: () => avatarUrl ?? account!.avatarUrl, + locale: locale ?? account!.locale, + ), + ); + _setResource(Resource(data: updated, isLoading: false)); + return updated; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future changePassword(String oldPassword, String newPassword) async { + if (account == null) throw ErrorUnauthorized(); + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final updated = await AccountService.changePassword(oldPassword, newPassword); + _setResource(Resource(data: updated, isLoading: false)); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future uploadAvatar(XFile avatarFile) async { + if (account == null) throw ErrorUnauthorized(); + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final avatarUrl = await AccountService.uploadAvatar(account!.id, avatarFile); + // Reuse the update method to update the avatar URL. + return update(avatarUrl: avatarUrl); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } +} diff --git a/frontend/pshared/lib/provider/accounts/employees.dart b/frontend/pshared/lib/provider/accounts/employees.dart new file mode 100644 index 0000000..f2c373d --- /dev/null +++ b/frontend/pshared/lib/provider/accounts/employees.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; + +import 'package:pshared/models/organization/employee.dart'; +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/accounts/employees.dart'; + + +class EmployeesProvider extends ChangeNotifier { + + Resource> _employees = Resource>(data: []); + List get employees => _employees.data ?? []; + bool get isLoading => _employees.isLoading; + Object? get error => _employees.error; + Employee? getEmployee(String? employeeRef) => employees.firstWhereOrNull((employee) => employee.id == employeeRef); + + + bool Function(Employee)? _filterPredicate; + + List get filteredItems => _filterPredicate != null + ? employees.where(_filterPredicate!).toList() + : employees; + + void setFilterPredicate(bool Function(Employee)? predicate) { + _filterPredicate = predicate; + notifyListeners(); + } + + void clearFilter() => setFilterPredicate(null); + + void updateProviders(OrganizationsProvider organizations) { + load(organizations.current.id); + } + + Future> load(String organizationRef) async { + _employees = _employees.copyWith(isLoading: true, error: null); + notifyListeners(); + + try { + final fetchedEmployees = await EmployeesService.list(organizationRef); + _employees = _employees.copyWith( + data: fetchedEmployees, + isLoading: false, + error: null, + ); + } catch (e) { + _employees = _employees.copyWith( + error: e is Exception ? e : Exception('Unknown error: ${e.toString()}'), + isLoading: false, + ); + } + + notifyListeners(); + return employees; + } +} diff --git a/frontend/pshared/lib/provider/exception.dart b/frontend/pshared/lib/provider/exception.dart new file mode 100644 index 0000000..22ba3e4 --- /dev/null +++ b/frontend/pshared/lib/provider/exception.dart @@ -0,0 +1,3 @@ +Exception toException(Object e) { + return e is Exception ? e : Exception(e.toString()); +} \ No newline at end of file diff --git a/frontend/pshared/lib/provider/locale.dart b/frontend/pshared/lib/provider/locale.dart new file mode 100644 index 0000000..277f033 --- /dev/null +++ b/frontend/pshared/lib/provider/locale.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/config/constants.dart'; + + +class LocaleProvider with ChangeNotifier { + Locale _locale = Constants.defaultLocale; + + Locale stringToLocale(String localeString) { + var parts = localeString.split(RegExp(r'[-_]')); + return (parts.length > 1) ? Locale(parts[0], parts[1]) : Locale(parts[0]); + } + + LocaleProvider(String? localeCode) { + if (localeCode != null) { + _locale = stringToLocale(localeCode); + } + } + + Locale get locale => _locale; + + void setLocale(Locale locale) { + if (_locale == locale) return; + + _locale = locale; + notifyListeners(); + } +} + diff --git a/frontend/pshared/lib/provider/organizations.dart b/frontend/pshared/lib/provider/organizations.dart new file mode 100644 index 0000000..c338885 --- /dev/null +++ b/frontend/pshared/lib/provider/organizations.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; + +import 'package:pshared/config/constants.dart'; +import 'package:pshared/models/organization/organization.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/provider/exception.dart'; +import 'package:pshared/service/organization.dart'; +import 'package:pshared/service/secure_storage.dart'; + + +class OrganizationsProvider extends ChangeNotifier { + Resource> _resource = Resource(data: []); + Resource> get resource => _resource; + + List get organizations => _resource.data ?? []; + String? _currentOrg; + + Organization get current => isOrganizationSet ? _current! : throw StateError('Organization is not set'); + + Organization? _org(String? orgRef) => organizations.firstWhereOrNull((org) => org.id == orgRef); + Organization? get _current => _org(_currentOrg); + + bool get isOrganizationSet => _current != null; + bool get isLoading => _resource.isLoading; + Object? get error => _resource.error; + + void _setResource(Resource> newResource) { + _resource = newResource; + notifyListeners(); + } + + Future> load() async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final orgs = await OrganizationService.list(); + // fetch stored org + String? org = await SecureStorageService.get(Constants.currentOrgKey); + // check stored org availability + org = orgs.firstWhereOrNull((o) => o.id == org)?.id; + // fallback if org is not set or not available + org ??= orgs.first.id; + await setCurrentOrganization(org); + _setResource(Resource(data: orgs, isLoading: false)); + return orgs; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future loadByInvitation(String invitationRef) async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final org = await OrganizationService.loadByInvitation(invitationRef); + await setCurrentOrganization(org.id); + _setResource(Resource(data: [org], isLoading: false)); + return org; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + bool _setCurrentOrganization(String? orgRef) { + final organizationRef = _org(orgRef)?.id; + if (organizationRef == null) return false; + + _currentOrg = organizationRef; + return true; + } + + Future setCurrentOrganization(String? orgRef) async { + if (!_setCurrentOrganization(orgRef)) return false; + await SecureStorageService.set(Constants.currentOrgKey, orgRef); + notifyListeners(); + return true; + } +} diff --git a/frontend/pshared/lib/provider/permissions.dart b/frontend/pshared/lib/provider/permissions.dart new file mode 100644 index 0000000..98091d4 --- /dev/null +++ b/frontend/pshared/lib/provider/permissions.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; + +import 'package:pshared/api/requests/change_role.dart'; +import 'package:pshared/models/permissions/access.dart'; +import 'package:pshared/models/permissions/action.dart' as perm; +import 'package:pshared/models/permissions/data/permission.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; +import 'package:pshared/models/permissions/data/role.dart'; +import 'package:pshared/models/permissions/descriptions/policy.dart'; +import 'package:pshared/models/permissions/descriptions/role.dart'; +import 'package:pshared/models/permissions/effect.dart'; +import 'package:pshared/models/resources.dart'; +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/permissions.dart'; + + +class PermissionsProvider extends ChangeNotifier { + Resource _userAccess = Resource(data: null, isLoading: false, error: null); + late OrganizationsProvider _organizations; + + void update(OrganizationsProvider venue) { + _organizations = venue; + } + + // Generic wrapper to perform service calls and reload state + Future _performServiceCall(Future Function() operation) async { + try { + await operation(); + return await load(); + } catch (e) { + _userAccess = _userAccess.copyWith( + error: e is Exception ? e : Exception(e.toString()), + isLoading: false, + ); + notifyListeners(); + return _userAccess.data; + } + } + + /// Load the [UserAccess] for the current venue. + Future load() async { + _userAccess = _userAccess.copyWith(isLoading: true, error: null); + notifyListeners(); + + try { + final orgRef = _organizations.current.id; + final access = await PermissionsService.load(orgRef); + _userAccess = _userAccess.copyWith(data: access, isLoading: false); + + if (canRead(ResourceType.roles)) { + final allAccess = await PermissionsService.loadAll(orgRef); + _userAccess = _userAccess.copyWith(data: allAccess, isLoading: false); + } + } catch (e) { + _userAccess = _userAccess.copyWith( + error: e is Exception ? e : Exception(e.toString()), + isLoading: false, + ); + } + + notifyListeners(); + return _userAccess.data; + } + + Future changeRole(String accountRef, String newRoleDescRef) async { + final currentRole = roles.firstWhereOrNull((r) => r.accountRef == accountRef); + final currentDesc = currentRole != null + ? roleDescriptions.firstWhereOrNull((d) => d.storable.id == currentRole.descriptionRef) + : null; + + if (currentRole == null || currentDesc == null || currentDesc.storable.id == newRoleDescRef) { + return _userAccess.data; + } + return _performServiceCall(() => PermissionsService.changeRole( + _organizations.current.id, + ChangeRole(accountRef: accountRef, newRoleDescriptionRef: newRoleDescRef), + )); + } + + Future deleteRoleDescription(String descRef) { + return _performServiceCall(() => PermissionsService.deleteRoleDescription(descRef)); + } + + Future createPermissions(List policies) { + return _performServiceCall(() => PermissionsService.createPolicies(policies)); + } + + Future deletePermissions(List policies) { + return _performServiceCall(() => PermissionsService.deletePolicies(policies)); + } + + Future changePermissions(List add, List remove) { + return _performServiceCall(() => PermissionsService.changePolicies(add, remove)); + } + + // -- Data getters -- + Set extractResourceTypes(Iterable descriptions) => descriptions.expand((policy) => policy.resourceTypes ?? []).toSet(); + + Set get resources => Set.unmodifiable(extractResourceTypes(policyDescriptions)); + + Set getRoleResources(String roleDescRef) => Set.unmodifiable( + extractResourceTypes( + getRolePermissions(roleDescRef) + .map((p) => getPolicyDescription(p.policy.descriptionRef)) + .whereType(), + ), + ); + + String? getPolicyDescriptionRef(ResourceType resource) => policyDescriptions.firstWhereOrNull((p) => p.resourceTypes?.contains(resource) ?? false)?.storable.id; + + List get policyDescriptions => List.unmodifiable(_userAccess.data?.descriptions.policies ?? []); + List get roleDescriptions => List.unmodifiable(_userAccess.data?.descriptions.roles ?? []); + List get permissions => List.unmodifiable(_userAccess.data?.permissions.permissions ?? []); + List get policies => List.unmodifiable(_userAccess.data?.permissions.policies ?? []); + List get roles => List.unmodifiable(_userAccess.data?.permissions.roles ?? []); + + Role? getRole(String accountRef) => roles.firstWhereOrNull((r) => r.accountRef == accountRef); + RoleDescription? getRoleDescription(String descRef) => roleDescriptions.firstWhereOrNull((d) => d.storable.id == descRef); + List getRoles(String accountRef) => roles.where((r) => r.accountRef == accountRef).toList(); + List getRolePolicies(String roleRef) => policies.where((p) => p.roleDescriptionRef == roleRef).toList(); + List getRolePermissions(String descRef) => permissions.where((p) => p.policy.roleDescriptionRef == descRef).toList(); + PolicyDescription? getPolicyDescription(String policyRef) => policyDescriptions.firstWhereOrNull((p) => p.storable.id == policyRef); + + // -- Permission checks -- + bool get isLoading => _userAccess.isLoading; + bool get isReady => !_userAccess.isLoading && error == null; + Exception? get error => _userAccess.error; + + bool _hasMatchingPermission( + PolicyDescription pd, + Effect effect, + perm.Action? action, { + Object? objectRef, + }) => permissions.firstWhereOrNull( + (p) => + p.policy.descriptionRef == pd.storable.id && + p.policy.effect.effect == effect && + (action == null || p.policy.effect.action == action) && + (p.policy.objectRef == null || p.policy.objectRef == objectRef), + ) != null; + + bool canAccessResource( + ResourceType resource, { + perm.Action? action, + Object? objectRef, + }) { + final orgId = _organizations.current.id; + final pd = policyDescriptions.firstWhereOrNull( + (policy) => + (policy.resourceTypes?.contains(resource) ?? false) && + (policy.organizationRef == null || policy.organizationRef == orgId), + ); + if (pd == null) return false; + if (_hasMatchingPermission(pd, Effect.deny, action, objectRef: objectRef)) return false; + return _hasMatchingPermission(pd, Effect.allow, action, objectRef: objectRef); + } + + bool canRead(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.read, objectRef: objectRef); + bool canUpdate(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.update, objectRef: objectRef); + bool canDelete(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.delete, objectRef: objectRef); + bool canCreate(ResourceType r) => canAccessResource(r, action: perm.Action.create); +} diff --git a/frontend/pshared/lib/provider/pfe/provider.dart b/frontend/pshared/lib/provider/pfe/provider.dart new file mode 100644 index 0000000..6fff15b --- /dev/null +++ b/frontend/pshared/lib/provider/pfe/provider.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/service/pfe/service.dart'; +import 'package:pshared/provider/exception.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/account.dart'; + + +class PfeProvider extends ChangeNotifier { + // The resource now wraps our Account? state along with its loading/error state. + Resource _resource = Resource(data: null); + Resource get resource => _resource; + + String? get session => _resource.data; + bool get isLoggedIn => session != null; + bool get isLoading => _resource.isLoading; + Object? get error => _resource.error; + + // Private helper to update the resource and notify listeners. + void _setResource(Resource newResource) { + _resource = newResource; + notifyListeners(); + } + + + Future login({ + required String email, + required String password, + }) async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + final acc = await PfeService.login(email, password); + _setResource(Resource(data: acc, isLoading: false)); + return acc; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future logout() async { + _setResource(_resource.copyWith(isLoading: true, error: null)); + try { + await AccountService.logout(); + _setResource(Resource(data: null, isLoading: false)); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } +} diff --git a/frontend/pshared/lib/provider/resource.dart b/frontend/pshared/lib/provider/resource.dart new file mode 100644 index 0000000..6d127f2 --- /dev/null +++ b/frontend/pshared/lib/provider/resource.dart @@ -0,0 +1,15 @@ +class Resource { + final T? data; + final bool isLoading; + final Exception? error; + + Resource({this.data, this.isLoading = false, this.error}); + + Resource copyWith({T? data, bool? isLoading, Exception? error}) { + return Resource( + data: data ?? this.data, + isLoading: isLoading ?? this.isLoading, + error: error ?? this.error, + ); + } +} diff --git a/frontend/pshared/lib/provider/services.dart b/frontend/pshared/lib/provider/services.dart new file mode 100644 index 0000000..e69de29 diff --git a/frontend/pshared/lib/provider/template.dart b/frontend/pshared/lib/provider/template.dart new file mode 100644 index 0000000..0a8d819 --- /dev/null +++ b/frontend/pshared/lib/provider/template.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; + +import 'package:pshared/models/permission_bound_storable.dart'; +import 'package:pshared/provider/exception.dart'; +import 'package:pshared/provider/resource.dart'; +import 'package:pshared/service/template.dart'; + + +List mergeLists({ + required List lhs, + required List rhs, + required Comparable Function(T) getKey, // Extracts ID dynamically + required int Function(T, T) compare, + required T Function(T, T) merge, +}) { + final result = []; + final map = {for (var item in lhs) getKey(item): item}; + + for (var updated in rhs) { + final key = getKey(updated); + map[key] = merge(map[key] ?? updated, updated); + } + + result.addAll(map.values); + result.sort(compare); + return result; +} + +/// A generic provider that wraps a [BasicService] instance +/// to manage state (loading, error, data) without re‑implementing service logic. +class GenericProvider extends ChangeNotifier { + final BasicService service; + + Resource> _resource = Resource(data: []); + Resource> get resource => _resource; + + List get items => List.unmodifiable(_resource.data ?? []); + bool get isLoading => _resource.isLoading; + bool get isEmpty => items.isEmpty; + Object? get error => _resource.error; + + String? _currentObjectRef; // Stores the currently selected project ref + T? get currentObject => _resource.data?.firstWhereOrNull( + (object) => object.id == _currentObjectRef, + ); + + T? getItemById(String id) => items.firstWhereOrNull((item) => item.id == id); + + GenericProvider({required this.service}); + + + bool Function(T)? _filterPredicate; + + List get filteredItems => _filterPredicate != null ? items.where(_filterPredicate!).toList() : items; + + void setFilterPredicate(bool Function(T)? predicate) { + _filterPredicate = predicate; + notifyListeners(); + } + + void clearFilter() => setFilterPredicate(null); + + void _setResource(Resource> newResource) { + _resource = newResource; + notifyListeners(); + } + + Future loadFuture(Future> future) async { + _setResource(_resource.copyWith(isLoading: true)); + try { + final list = await future; + _setResource(Resource(data: list, isLoading: false)); + } catch (e) { + _setResource( + _resource.copyWith(isLoading: false, error: toException(e)), + ); + rethrow; + } + } + + Future load(String organizationRef, String? parentRef) async { + if (parentRef != null) { + return loadFuture(service.list(organizationRef, parentRef)); + } + } + + Future loadItem(String itemRef) async { + return loadFuture((() async => [await service.get(itemRef)])()); + } + + + List merge(List rhs) => mergeLists( + lhs: items, + rhs: rhs, + getKey: (item) => item.id, // Key extractor + compare: (a, b) => a.id.compareTo(b.id), // Sorting logic + merge: (existing, updated) => updated, // Replace with the updated version + ); + + Future get(String objectRef) async { + _setResource(_resource.copyWith(isLoading: true)); + try { + final item = await service.get(objectRef); + _setResource(Resource(data: merge([item]), isLoading: false)); + return item; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future createObject(String organizationRef, Map request) async { + _setResource(_resource.copyWith(isLoading: true)); + try { + final newObject = await service.create(organizationRef, request); + _setResource(Resource(data: [...items, ...newObject], isLoading: false)); + return newObject.first; + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future update(Map request) async { + _setResource(_resource.copyWith(isLoading: true)); + try { + final list = await service.update(request); + _setResource(Resource(data: merge(list), isLoading: false)); + } catch (e) { + _setResource(_resource.copyWith(isLoading: false, error: toException(e))); + rethrow; + } + } + + Future delete(String objectRef) async { + _setResource(_resource.copyWith(isLoading: true)); + + try { + await service.delete(objectRef); + if (_currentObjectRef == objectRef) { + _currentObjectRef = null; + } + + _setResource(Resource( + data: _resource.data?.where((p) => p.id != objectRef).toList(), + isLoading: false, + )); + } catch (e) { + _setResource(Resource(data: _resource.data, isLoading: false, error: toException(e))); + rethrow; + } + } + + bool setCurrentObject(String? objectRef) { + if (objectRef == null) { + _currentObjectRef = null; + notifyListeners(); + return true; + } + if (_resource.data?.any((p) => p.id == objectRef) ?? false) { + _currentObjectRef = objectRef; + notifyListeners(); + return true; + } + + return false; // Object not found + } +} diff --git a/frontend/pshared/lib/pshared.dart b/frontend/pshared/lib/pshared.dart new file mode 100644 index 0000000..baa1a9d --- /dev/null +++ b/frontend/pshared/lib/pshared.dart @@ -0,0 +1,8 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'utils/http/requests.dart'; + +// TODO: Export any libraries intended for clients of this package. diff --git a/frontend/pshared/lib/service/account.dart b/frontend/pshared/lib/service/account.dart new file mode 100644 index 0000000..a51ccff --- /dev/null +++ b/frontend/pshared/lib/service/account.dart @@ -0,0 +1,61 @@ +import 'package:logging/logging.dart'; + +import 'package:share_plus/share_plus.dart'; + +import 'package:pshared/api/requests/signup.dart'; +import 'package:pshared/api/responses/account.dart'; +import 'package:pshared/api/requests/change_password.dart'; +import 'package:pshared/data/mapper/account/account.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/service/files.dart'; +import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/http/requests.dart'; + + +class AccountService { + static final _logger = Logger('service.account'); + static const String _objectType = Services.account; + + static Future login(String email, String password, String locale) async { + _logger.fine('Logging in'); + return AuthorizationService.login(_objectType, email, password, locale); + } + + static Future restore() async { + return AuthorizationService.restore(); + } + + static Future signup(SignupRequest request) async { + await getPOSTResponse(_objectType, 'signup', request.toJson()); + } + + static Future logout() async { + _logger.fine('Logging out'); + await AuthorizationService.logout(); + } + + static Future _getAccount(Future> future) async { + final response = await future; + return AccountResponse.fromJson(response).account.toDomain(); + } + + static Future update(Account account) async { + _logger.fine('Patching account ${account.id}'); + return _getAccount(AuthorizationService.getPUTResponse(_objectType, '', account.toDTO().toJson())); + } + + static Future changePassword(String oldPassword, String newPassword) async { + _logger.fine('Changing password'); + return _getAccount(AuthorizationService.getPATCHResponse( + _objectType, + 'password', + ChangePassword(oldPassword: oldPassword, newPassword: newPassword).toJson(), + )); + } + + static Future uploadAvatar(String id, XFile avatarFile) async { + _logger.fine('Uploading avatar'); + return FilesService.uploadImage(_objectType, id, avatarFile); + } +} diff --git a/frontend/pshared/lib/service/accounts/employees.dart b/frontend/pshared/lib/service/accounts/employees.dart new file mode 100644 index 0000000..b7cf7ba --- /dev/null +++ b/frontend/pshared/lib/service/accounts/employees.dart @@ -0,0 +1,35 @@ +import 'package:logging/logging.dart'; + +import 'package:pshared/api/errors/unauthorized.dart'; +import 'package:pshared/api/responses/employees.dart'; +import 'package:pshared/models/organization/employee.dart'; +import 'package:pshared/data/mapper/account/account.dart'; +import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/service/services.dart'; + + +class EmployeesService { + static final _logger = Logger('service.employees'); + static const String _objectType = Services.account; + + static Future> list(String organizationRef) async { + _logger.fine('Loading organization employees'); + return _getEmployees(AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef')); + } + + + static Future> _getEmployees(Future> future) async { + try { + final responseJson = await future; + final response = EmployeesResponse.fromJson(responseJson); + final accounts = response.accounts.map((dto) => dto.toDomain()).toList(); + + if (accounts.isEmpty) throw ErrorUnauthorized(); + _logger.fine('Fetched ${accounts.length} account(s)'); + return accounts; + } catch (e, stackTrace) { + _logger.severe('Failed to fetch accounts', e, stackTrace); + rethrow; + } + } +} diff --git a/frontend/pshared/lib/service/authorization/service.dart b/frontend/pshared/lib/service/authorization/service.dart new file mode 100644 index 0000000..79ba68a --- /dev/null +++ b/frontend/pshared/lib/service/authorization/service.dart @@ -0,0 +1,89 @@ +import 'package:logging/logging.dart'; + +import 'package:pshared/api/errors/upload_failed.dart'; +import 'package:pshared/api/requests/login.dart'; +import 'package:pshared/api/responses/account.dart'; +import 'package:pshared/api/responses/login.dart'; +import 'package:pshared/config/constants.dart'; +import 'package:pshared/data/mapper/account/account.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/service/authorization/storage.dart'; +import 'package:pshared/service/authorization/token.dart'; +import 'package:pshared/service/device_id.dart'; +import 'package:pshared/utils/http/requests.dart' as httpr; + + +class AuthorizationService { + static final _logger = Logger('service.authorization'); + + static Future _updateAccessToken(AccountResponse response) async { + await AuthorizationStorage.updateToken(response.accessToken); + } + + static Future _updateTokens(LoginResponse response) async { + await _updateAccessToken(response); + return AuthorizationStorage.updateRefreshToken(response.refreshToken); + } + + static Future _completeLogin(Map response) async { + final LoginResponse lr = LoginResponse.fromJson(response); + await _updateTokens(lr); + return lr; + } + + static Future login(String service, String email, String password, String locale) async { + _logger.fine('Logging in $email with $locale locale'); + final deviceId = await DeviceIdManager.getDeviceId(); + final response = await httpr.getPOSTResponse( + service, + '/login', + LoginRequest( + login: email.toLowerCase(), + password: password, + locale: locale, + deviceId: deviceId, + clientId: Constants.clientId, + ).toJson()); + + return (await _completeLogin(response)).account.toDomain(); + } + + static Future restore() async { + return (await TokenService.rotateRefreshToken()).account.toDomain(); + } + + static Future logout() async { + return AuthorizationStorage.removeTokens(); + } + + static Future> _authenticatedRequest( + String service, + String url, + Future> Function(String, String, Map, {String? authToken}) requestType, + {Map? body}) async { + final accessToken = await TokenService.getAccessToken(); + return requestType(service, url, body ?? {}, authToken: accessToken); + } + + static Future> getPOSTResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPOSTResponse, body: body); + + static Future> getGETResponse(String service, String url) async { + final accessToken = await TokenService.getAccessToken(); + return httpr.getGETResponse(service, url, authToken: accessToken); + } + + static Future> getPUTResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPUTResponse, body: body); + + static Future> getPATCHResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getPATCHResponse, body: body); + + static Future> getDELETEResponse(String service, String url, Map body) async => _authenticatedRequest(service, url, httpr.getDELETEResponse, body: body); + + static Future getFileUploadResponseAuth(String service, String url, String fileName, String fileType, String mediaType, List bytes) async { + final accessToken = await TokenService.getAccessToken(); + final res = await httpr.getFileUploadResponse(service, url, fileName, fileType, mediaType, bytes, authToken: accessToken); + if (res == null) { + throw ErrorUploadFailed(); + } + return res.url; + } +} diff --git a/frontend/pshared/lib/service/authorization/storage.dart b/frontend/pshared/lib/service/authorization/storage.dart new file mode 100644 index 0000000..bb49431 --- /dev/null +++ b/frontend/pshared/lib/service/authorization/storage.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:logging/logging.dart'; + +import 'package:pshared/api/errors/unauthorized.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/config/constants.dart'; +import 'package:pshared/service/secure_storage.dart'; + + +class AuthorizationStorage { + static final _logger = Logger('service.authorization.storage'); + + static Future _getTokenData(String tokenKey) async { + _logger.fine('Getting token data'); + final String? tokenJson = await SecureStorageService.get(tokenKey); + if (tokenJson == null || tokenJson.isEmpty) { + throw ErrorUnauthorized(); + } + return TokenData.fromJson(jsonDecode(tokenJson)); + } + + static Future getAccessToken() async { + _logger.fine('Getting access token'); + return _getTokenData(Constants.accessTokenStorageKey); + } + + static Future getRefreshToken() async { + _logger.fine('Getting refresh token'); + return _getTokenData(Constants.refreshTokenStorageKey); + } + + static Future updateToken(TokenData tokenData) async { + _logger.fine('Storing access token...'); + final tokenJson = jsonEncode(tokenData.toJson()); + await SecureStorageService.set(Constants.accessTokenStorageKey, tokenJson); + } + + static Future updateRefreshToken(TokenData tokenData) async { + _logger.fine('Storing refresh token...'); + final refreshTokenJson = jsonEncode(tokenData.toJson()); + await SecureStorageService.set(Constants.refreshTokenStorageKey, refreshTokenJson); + } + + static Future removeTokens() { + return Future.wait([ + SecureStorageService.delete(Constants.refreshTokenStorageKey), + SecureStorageService.delete(Constants.accessTokenStorageKey), + ]); + } +} + + diff --git a/frontend/pshared/lib/service/authorization/token.dart b/frontend/pshared/lib/service/authorization/token.dart new file mode 100644 index 0000000..0357e98 --- /dev/null +++ b/frontend/pshared/lib/service/authorization/token.dart @@ -0,0 +1,85 @@ + + +import 'package:logging/logging.dart'; + +import 'package:pshared/api/errors/unauthorized.dart'; +import 'package:pshared/api/requests/tokens/access_refresh.dart'; +import 'package:pshared/api/requests/tokens/refresh_rotate.dart'; +import 'package:pshared/api/responses/account.dart'; +import 'package:pshared/api/responses/login.dart'; +import 'package:pshared/api/responses/token.dart'; +import 'package:pshared/config/constants.dart'; +import 'package:pshared/service/authorization/storage.dart'; +import 'package:pshared/service/device_id.dart'; +import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/http/requests.dart'; + + +class TokenService { + static final _logger = Logger('service.authorization.token'); + static const String _objectType = Services.account; + + static bool _isTokenExpiringSoon(TokenData token, Duration duration) { + return token.expiration.isBefore(DateTime.now().add(duration)); + } + + static Future getAccessToken() async { + TokenData token = await AuthorizationStorage.getAccessToken(); + if (_isTokenExpiringSoon(token, const Duration(hours: 4))) { + token = (await _refreshAccessToken()).accessToken; + } + return token.token; + } + + static Future _updateTokens(LoginResponse response) async { + await AuthorizationStorage.updateToken(response.accessToken); + await AuthorizationStorage.updateRefreshToken(response.refreshToken); + } + + static Future _refreshAccessToken() async { + _logger.fine('Refreshing access token...'); + final deviceId = await DeviceIdManager.getDeviceId(); + final refresh = await AuthorizationStorage.getRefreshToken(); + + if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) { + return await rotateRefreshToken(); + } + + final response = await getPOSTResponse( + _objectType, + '/refresh', + AccessTokenRefreshRequest( + deviceId: deviceId, + clientId: Constants.clientId, + token: refresh.token, + ).toJson(), + ); + + final accountResp = AccountResponse.fromJson(response); + await AuthorizationStorage.updateToken(accountResp.accessToken); + return accountResp; + } + + static Future rotateRefreshToken() async { + _logger.fine('Rotating refresh token...'); + final refresh = await AuthorizationStorage.getRefreshToken(); + + if (refresh.expiration.isBefore(DateTime.now())) throw ErrorUnauthorized(); + + final deviceId = await DeviceIdManager.getDeviceId(); + final response = await getPOSTResponse( + _objectType, + '/rotate', + RotateRefreshTokenRequest( + token: refresh.token, + clientId: Constants.clientId, + deviceId: deviceId, + ).toJson(), + ); + + final loginResponse = LoginResponse.fromJson(response); + await _updateTokens(loginResponse); + return loginResponse; + } + +} diff --git a/frontend/pshared/lib/service/device_id.dart b/frontend/pshared/lib/service/device_id.dart new file mode 100644 index 0000000..d6cf635 --- /dev/null +++ b/frontend/pshared/lib/service/device_id.dart @@ -0,0 +1,24 @@ +import 'package:uuid/uuid.dart'; + +import 'package:logging/logging.dart'; + +import 'package:pshared/config/web.dart'; +import 'package:pshared/service/secure_storage.dart'; + + +class DeviceIdManager { + static final _logger = Logger('service.device_id'); + + static final String _key = Constants.deviceIdStorageKey; + static Future getDeviceId() async { + String? deviceId = await SecureStorageService.get(_key); + + if (deviceId == null) { + _logger.fine('Device id is not set, generating new'); + deviceId = (const Uuid()).v4(); + await SecureStorageService.set(_key, deviceId); + } + + return deviceId; + } +} diff --git a/frontend/pshared/lib/service/files.dart b/frontend/pshared/lib/service/files.dart new file mode 100644 index 0000000..b4ed7b1 --- /dev/null +++ b/frontend/pshared/lib/service/files.dart @@ -0,0 +1,38 @@ +import 'dart:math'; + +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:share_plus/share_plus.dart'; + +import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/utils/image/conversion.dart'; +import 'package:pshared/utils/image/transformed.dart'; + + +String generateRandomLatinString(int length) { + const String chars = 'abcdefghijklmnopqrstuvwxyz'; + final Random random = Random.secure(); + return List.generate(length, (index) => chars[random.nextInt(chars.length)]).join(); +} + +class FilesService { + static Future uploadImage( + String objectType, + String? id, + XFile imageFile, { + Future Function(XFile) fileReader = defaultTransformImage, + }) async { + final objRef = id ?? generateRandomLatinString(16); + final image = await fileReader(imageFile); + final res = await AuthorizationService.getFileUploadResponseAuth( + objectType, + 'image/$objRef', + '$objRef.${image.imageType.split('/').last}', + 'image', + image.imageType, + image.bytes + ); + CachedNetworkImage.evictFromCache(res); + return res; + } +} diff --git a/frontend/pshared/lib/service/organization.dart b/frontend/pshared/lib/service/organization.dart new file mode 100644 index 0000000..d34b42f --- /dev/null +++ b/frontend/pshared/lib/service/organization.dart @@ -0,0 +1,63 @@ +import 'package:logging/logging.dart'; + +import 'package:share_plus/share_plus.dart'; + +import 'package:pshared/api/errors/unauthorized.dart'; +import 'package:pshared/api/responses/organization.dart'; +import 'package:pshared/models/organization/organization.dart'; +import 'package:pshared/data/mapper/organization.dart'; +import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/service/files.dart'; +import 'package:pshared/service/services.dart'; +import 'package:pshared/utils/http/requests.dart'; + + +class OrganizationService { + static final _logger = Logger('service.organization'); + static const String _objectType = Services.organization; + + static Future> list() async { + _logger.fine('Loading all organizations'); + return _getOrganizations(AuthorizationService.getGETResponse(_objectType, '')); + } + + static Future load(String organizationRef) async { + _logger.fine('Loading organization $organizationRef'); + final orgs = await _getOrganizations(AuthorizationService.getGETResponse(_objectType, organizationRef)); + return orgs.first; + } + + static Future loadByInvitation(String invitationRef) async { + _logger.fine('Loading organization by invitation ref $invitationRef'); + final orgs = await _getOrganizations(getGETResponse(_objectType, 'invitation/$invitationRef')); + return orgs.first; + } + + static Future> update(Organization organization) async { + _logger.fine('Patching organization ${organization.id}'); + // Convert domain object to DTO, then to JSON + return _getOrganizations( + AuthorizationService.getPUTResponse(_objectType, '', organization.toDTO().toJson()) + ); + } + + static Future uploadLogo(String organizationRef, XFile logoFile) async { + _logger.fine('Uploading logo'); + return FilesService.uploadImage(_objectType, organizationRef, logoFile); + } + + static Future> _getOrganizations(Future> future) async { + try { + final responseJson = await future; + final response = OrganizationResponse.fromJson(responseJson); + final orgs = response.organizations.map((dto) => dto.toDomain()).toList(); + + if (orgs.isEmpty) throw ErrorUnauthorized(); + _logger.fine('Fetched ${orgs.length} organization(s)'); + return orgs; + } catch (e, stackTrace) { + _logger.severe('Failed to fetch organizations', e, stackTrace); + rethrow; + } + } +} diff --git a/frontend/pshared/lib/service/permissions.dart b/frontend/pshared/lib/service/permissions.dart new file mode 100644 index 0000000..7cbeefc --- /dev/null +++ b/frontend/pshared/lib/service/permissions.dart @@ -0,0 +1,79 @@ +import 'package:logging/logging.dart'; + +import 'package:pshared/api/requests/change_role.dart'; +import 'package:pshared/api/requests/permissions/change_policies.dart'; +import 'package:pshared/api/responses/policies.dart'; +import 'package:pshared/data/mapper/permissions/data/permissions.dart'; +import 'package:pshared/data/mapper/permissions/descriptions/description.dart'; +import 'package:pshared/models/permissions/access.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; +import 'package:pshared/service/authorization/service.dart'; +import 'package:pshared/service/services.dart'; + + +class PermissionsService { + static final _logger = Logger('service.permissions'); + static const String _objectType = Services.permission; + + static Future load(String organizationRef) async { + _logger.fine('Loading permissions...'); + return _getPolicies(AuthorizationService.getGETResponse(_objectType, organizationRef)); + } + + static Future loadAll(String organizationRef) async { + _logger.fine('Loading permissions for all the users...'); + return _getPolicies(AuthorizationService.getGETResponse(_objectType, '/all/$organizationRef')); + } + + static Future changeRole(String organizationRef, ChangeRole request) async { + _logger.fine('Changing role for account ${request.accountRef} to role ${request.newRoleDescriptionRef}'); + await AuthorizationService.getPOSTResponse(_objectType, '/change_role/$organizationRef', request.toJson()); + } + + static Future deleteRoleDescription(String roleDescriptionRef) async { + _logger.fine('Deleting role $roleDescriptionRef...'); + await AuthorizationService.getDELETEResponse(_objectType, '/role/$roleDescriptionRef', {}); + } + + static Future createPolicies(List policies) async { + _logger.fine('Creating ${policies.length} policies...'); + await AuthorizationService.getPOSTResponse( + _objectType, + '/policies', + PoliciesChangeRequest.add(policies: policies).toJson(), + ); + } + + static Future deletePolicies(List policies) async { + _logger.fine('Deleting ${policies.length} policies...'); + await AuthorizationService.getDELETEResponse( + _objectType, + '/policies', + PoliciesChangeRequest.remove(policies: policies).toJson(), + ); + } + + static Future changePolicies(List add, List remove) async { + final common = add.toSet().intersection(remove.toSet()); + if (common.isNotEmpty) { + throw ArgumentError.value(common, 'add/remove', 'These policies are in both add and remove: ${common.toString()}'); + } + _logger.fine('Adding ${add.length} policies, removing ${remove.length} policies...'); + await AuthorizationService.getPUTResponse( + _objectType, + '/policies', + PoliciesChangeRequest.change(add: add, remove: remove).toJson(), + ); + } + + static Future _getPolicies(Future> future) async { + final resp = PoliciesResponse.fromJson(await future); + final res = UserAccess( + descriptions: resp.descriptions.toDomain(), + permissions: resp.permissions.toDomain(), + ); + _logger.fine('Loaded ${res.descriptions.roles.length} role descriptions, ${res.permissions.roles.length} role assignments, ${res.descriptions.policies.length} policy descriptions, ${res.permissions.policies.length} assigned policies, and ${res.permissions.permissions.length} assigned permissions'); + + return res; + } +} diff --git a/frontend/pshared/lib/service/pfe/login.dart b/frontend/pshared/lib/service/pfe/login.dart new file mode 100644 index 0000000..5f111aa --- /dev/null +++ b/frontend/pshared/lib/service/pfe/login.dart @@ -0,0 +1,15 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login.g.dart'; + + +@JsonSerializable() +class LoginRequest { + final String login; + final String password; + + const LoginRequest({required this.login, required this.password}); + + factory LoginRequest.fromJson(Map json) => _$LoginRequestFromJson(json); + Map toJson() => _$LoginRequestToJson(this); +} diff --git a/frontend/pshared/lib/service/pfe/service.dart b/frontend/pshared/lib/service/pfe/service.dart new file mode 100644 index 0000000..fc95f61 --- /dev/null +++ b/frontend/pshared/lib/service/pfe/service.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:logging/logging.dart'; + +import 'package:http/http.dart' as http; + +import 'package:pshared/service/pfe/login.dart'; + + +class PfeService { + static final _logger = Logger('service.pfe'); + + static Future login(String email, String password) async { + _logger.fine('Logging in'); + + try { + final res = await http.post( + Uri.parse('http://localhost:3000/api/v1/auth/login'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(LoginRequest(login: email, password: password).toJson()), + ); + return res.toString(); + } catch (e) { + _logger.warning(e.toString()); + rethrow; + } + + } +} diff --git a/frontend/pshared/lib/service/pfe/services.dart b/frontend/pshared/lib/service/pfe/services.dart new file mode 100644 index 0000000..e69de29 diff --git a/frontend/pshared/lib/service/secure_storage.dart b/frontend/pshared/lib/service/secure_storage.dart new file mode 100644 index 0000000..fa0f9e0 --- /dev/null +++ b/frontend/pshared/lib/service/secure_storage.dart @@ -0,0 +1,25 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class SecureStorageService { + static Future get(String key) async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(key); + } + + static Future _setImp(SharedPreferences prefs, String key, String value) async { + await prefs.setString(key, value); + } + + static Future set(String key, String? value) async { + final prefs = await SharedPreferences.getInstance(); + if (value == null) { + return delete(key); + } + return _setImp(prefs, key, value); + } + + static Future delete(String key) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(key); + } +} diff --git a/frontend/pshared/lib/service/services.dart b/frontend/pshared/lib/service/services.dart new file mode 100644 index 0000000..efeb745 --- /dev/null +++ b/frontend/pshared/lib/service/services.dart @@ -0,0 +1,32 @@ +class Services { + static const String account = 'accounts'; + static const String authorization = 'authorization'; + static const String comments = 'comments'; + static const String device = 'device'; + static const String invitations = 'invitations'; + static const String organization = 'organizations'; + static const String permission = 'permissions'; + static const String project = 'projects'; + static const String pgroup = 'priority_groups'; + static const String priorities = 'priorities'; + static const String reactions = 'reactions'; + static const String storage = 'storage'; + static const String taskStatus = 'statuses'; + static const String tasks = 'tasks'; + + static const String amplitude = 'amplitude'; + static const String automations = 'automation'; + static const String changes = 'changes'; + static const String clients = 'clients'; + static const String invoices = 'invoices'; + static const String logo = 'logo'; + static const String notifications = 'notifications'; + static const String policies = 'policies'; + static const String properties = 'properties'; + static const String refreshTokens = 'refresh_tokens'; + static const String roles = 'roles'; + static const String steps = 'steps'; + static const String teams = 'teams'; + static const String workflows = 'workflows'; + static const String workspaces = 'workspaces'; +} diff --git a/frontend/pshared/lib/service/template.dart b/frontend/pshared/lib/service/template.dart new file mode 100644 index 0000000..d61a2aa --- /dev/null +++ b/frontend/pshared/lib/service/template.dart @@ -0,0 +1,66 @@ +import 'package:logging/logging.dart'; + +import 'package:pshared/service/authorization/service.dart'; + + +class BasicService { + final String _objectType; + final Logger _logger; + final List Function(Map json) fromJson; + + Logger get logger => _logger; + + BasicService({ + required String objectType, + required this.fromJson, + }) : _objectType = objectType, _logger = Logger('service.$objectType'); + + Future> list(String organizationRef, String parentRef) async { + _logger.fine('Loading all objects'); + return _getObjects( + AuthorizationService.getGETResponse(_objectType, '/list/$organizationRef/$parentRef'), + ); + } + + Future get(String objectRef) async { + _logger.fine('Loading object $objectRef'); + final objects = await _getObjects( + AuthorizationService.getGETResponse(_objectType, '/$objectRef'), + ); + return objects.first; + } + + Future> create(String organizationRef, Map request) async { + _logger.fine('Creating new object...'); + return _getObjects( + AuthorizationService.getPOSTResponse(_objectType, '/$organizationRef', request), + ); + } + + Future> update(Map request) async { + _logger.fine('Patching object...'); + return _getObjects( + AuthorizationService.getPUTResponse(_objectType, '/', request, + ), + ); + } + + Future> delete(String objecRef) async { + _logger.fine('Deleting object $objecRef'); + return _getObjects( + AuthorizationService.getDELETEResponse(_objectType, '/$objecRef', {}), + ); + } + + Future> _getObjects(Future> future) async { + try { + final responseJson = await future; + final objects = fromJson(responseJson); + _logger.fine('Fetched ${objects.length} object(s)'); + return objects; + } catch (e, stackTrace) { + _logger.severe('Failed to fetch objects', e, stackTrace); + rethrow; + } + } +} diff --git a/frontend/pshared/lib/utils/clipboard.dart b/frontend/pshared/lib/utils/clipboard.dart new file mode 100644 index 0000000..9b71a63 --- /dev/null +++ b/frontend/pshared/lib/utils/clipboard.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:pshared/utils/snackbar.dart'; + + +Future copyToClipboard(BuildContext context, String text, String hint, {int delaySeconds = 3}) async { + final res = Clipboard.setData(ClipboardData(text: text)); + notifyUser(context, hint, delaySeconds: delaySeconds); + return res; +} \ No newline at end of file diff --git a/frontend/pshared/lib/utils/currency.dart b/frontend/pshared/lib/utils/currency.dart new file mode 100644 index 0000000..af50a31 --- /dev/null +++ b/frontend/pshared/lib/utils/currency.dart @@ -0,0 +1,22 @@ +String currencyCodeToSymbol(String currencyCode) { + switch (currencyCode) { + case 'USD': + return '\$'; + case 'PLN': + return 'zł'; + case 'EUR': + return '€'; + case 'GBP': + return '£'; + case 'HUF': + return 'Ft'; + case 'RUB': + return '₽'; + default: + return currencyCode; + } +} + +String currencyToString(String currencyCode, double amount) { + return '${currencyCodeToSymbol(currencyCode)}\u00A0${amount.toStringAsFixed(2)}'; +} \ No newline at end of file diff --git a/frontend/pshared/lib/utils/datetime_serializer.dart b/frontend/pshared/lib/utils/datetime_serializer.dart new file mode 100644 index 0000000..6cc18a2 --- /dev/null +++ b/frontend/pshared/lib/utils/datetime_serializer.dart @@ -0,0 +1,9 @@ +extension DateTimeSerializer on DateTime { + static String toBackendString(DateTime dt) { + return dt.toUtc().toIso8601String(); + } + + static DateTime fromBackendString(String dateStr) { + return DateTime.parse(dateStr); + } +} diff --git a/frontend/pshared/lib/utils/flagged_locale.dart b/frontend/pshared/lib/utils/flagged_locale.dart new file mode 100644 index 0000000..e307953 --- /dev/null +++ b/frontend/pshared/lib/utils/flagged_locale.dart @@ -0,0 +1,91 @@ +import 'package:intl/intl.dart'; + +import 'package:flutter/material.dart'; + +import 'package:jovial_svg/jovial_svg.dart'; + +import 'package:country_flags/country_flags.dart'; + + +String _locale2Flag(Locale l) { + if (l.languageCode == 'en') { + return 'gb'; + } + if (l.languageCode == 'uk') { + return 'ua'; + } + if (l.languageCode == 'el') { + return 'gr'; + } + return l.languageCode; +} + +final Map localeNames = { + 'ca': 'Català', + 'en': 'English', + 'es': 'Español', + 'fr': 'Français', + 'de': 'Deutsch', + 'uk': 'Українська', + 'el': 'Ελληνικά', + 'ru': 'Русский', + 'pt': 'Português', + 'pl': 'Polski', + 'it': 'Italiano', + 'nl': 'Nederlands', +}; + +class _CatalanFlag extends StatelessWidget { + final double? width; + final double? height; + final BoxFit? fit; + + _CatalanFlag({ + required this.width, + required this.height, + required this.fit, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + height: height, + child: ScalableImageWidget.fromSISource( + key: const Key('svgFlagCa'), + si: ScalableImageSource.fromSI( + DefaultAssetBundle.of(context), + 'packages/pshared/assets/flag_of_catalonia.si', + ), + fit: BoxFit.contain, + ), + ); + } +} + +Widget getCountryFlag(Locale locale, {double? height = 24, double? width = 35}) { + return locale.languageCode.toLowerCase() == 'ca' + ? _CatalanFlag(width: width, height: height, fit: BoxFit.contain) + : CountryFlag.fromCountryCode( + _locale2Flag(locale), + height: height, + width: width, + shape: Rectangle(), + ); +} + +String getLocaleName(Locale locale) { + return localeNames[locale.languageCode] ?? Intl.canonicalizedLocale(locale.toString()).toUpperCase(); +} + +Widget getLocaleNameWidget(Locale locale) { + return Text(getLocaleName(locale), overflow: TextOverflow.ellipsis); +} + +Widget getFlaggedLocale(Locale locale) { + return ListTile( + leading: getCountryFlag(locale), + title: getLocaleNameWidget(locale), + ); +} + diff --git a/frontend/pshared/lib/utils/http/client.dart b/frontend/pshared/lib/utils/http/client.dart new file mode 100644 index 0000000..c9d5e0d --- /dev/null +++ b/frontend/pshared/lib/utils/http/client.dart @@ -0,0 +1,2 @@ +export 'client/io.dart' + if (dart.library.html) 'http_client/web.dart'; \ No newline at end of file diff --git a/frontend/pshared/lib/utils/http/client/io.dart b/frontend/pshared/lib/utils/http/client/io.dart new file mode 100644 index 0000000..9f66e3f --- /dev/null +++ b/frontend/pshared/lib/utils/http/client/io.dart @@ -0,0 +1,55 @@ +import 'dart:io' as io show HttpClient, HttpHeaders; + +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + + +const _sessionCookie = 'session_id'; + +@override +http.Client buildHttpClient() => _SessionClient(IOClient(io.HttpClient())); + +class _SessionClient extends http.BaseClient { + final http.Client _inner; + String? _sessionId; + + _SessionClient(this._inner); + + @override + Future send(http.BaseRequest request) async { + if (_sessionId != null) { + request.headers[io.HttpHeaders.cookieHeader] = '$_sessionCookie=$_sessionId'; + } + + request.followRedirects = false; + request.maxRedirects = 0; + + http.StreamedResponse response = await _inner.send(request); + + _captureCookie(response.headers[io.HttpHeaders.setCookieHeader]); + + while (response.isRedirect) { + final location = response.headers['location']; + if (location == null) break; + + final redirected = http.Request(request.method, Uri.parse(location)) + ..headers.addAll(request.headers) + ..bodyBytes = await response.stream.toBytes() + ..followRedirects = false + ..maxRedirects = 0; + + response = await _inner.send(redirected); + _captureCookie(response.headers[io.HttpHeaders.setCookieHeader]); + } + + return response; + } + + void _captureCookie(String? setCookieHeader) { + if (setCookieHeader == null) return; + final match = RegExp('$_sessionCookie=([^;]+)', + caseSensitive: false) + .firstMatch(setCookieHeader); + if (match != null) _sessionId = match.group(1); + } +} diff --git a/frontend/pshared/lib/utils/http/client/stub.dart b/frontend/pshared/lib/utils/http/client/stub.dart new file mode 100644 index 0000000..c669fb7 --- /dev/null +++ b/frontend/pshared/lib/utils/http/client/stub.dart @@ -0,0 +1,4 @@ +import 'package:http/http.dart'; + + +Client buildHttpClient() => throw UnsupportedError('buildHttpClient() was called without a proper platform implementation.'); diff --git a/frontend/pshared/lib/utils/http/client/web.dart b/frontend/pshared/lib/utils/http/client/web.dart new file mode 100644 index 0000000..3094e38 --- /dev/null +++ b/frontend/pshared/lib/utils/http/client/web.dart @@ -0,0 +1,9 @@ +import 'package:http/browser_client.dart'; +import 'package:http/http.dart' as http; + +@override +http.Client buildHttpClient() { + final bc = BrowserClient(); + bc.withCredentials = true; + return bc; +} diff --git a/frontend/pshared/lib/utils/http/requests.dart b/frontend/pshared/lib/utils/http/requests.dart new file mode 100644 index 0000000..7902d64 --- /dev/null +++ b/frontend/pshared/lib/utils/http/requests.dart @@ -0,0 +1,165 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import 'package:http_parser/http_parser.dart'; + +import 'package:pshared/api/responses/file_uploaded.dart'; +import 'package:pshared/api/responses/message.dart'; +import 'package:pshared/api/responses/error/connectivity.dart'; +import 'package:pshared/api/responses/error/server.dart'; +import 'package:pshared/config/constants.dart'; + + +Uri _uri(String service, String url) { + // Ensure the base URL ends with a slash + final normalizedBaseUrl = Constants.apiUrl.endsWith('/') + ? Constants.apiUrl + : '${Constants.apiUrl}/'; + + // Remove leading slash from service, if any + final normalizedService = service.startsWith('/') ? service.substring(1) : service; + + // Remove leading slash from url, if any + final normalizedUrl = url.startsWith('/') ? url.substring(1) : url; + + // Only append a trailing slash to the service + // if the URL is non-empty + final serviceWithOptionalSlash = normalizedUrl.isNotEmpty ? '$normalizedService/' : normalizedService; + + return Uri.parse(normalizedBaseUrl).resolve(serviceWithOptionalSlash).resolve(normalizedUrl); +} + + + +Map _authHeader(Map headers, String? authToken) { + if (authToken != null && authToken.isNotEmpty) { + headers['Authorization'] = 'Bearer $authToken'; + } + return headers; +} + +Map _headers({String? authToken}) { + final headers = {'Content-Type': 'application/json'}; + return _authHeader(headers, authToken); +} + +Future postRequest(String service, String url, Map body, {String? authToken}) async { + final response = await http.post(_uri(service, url), + headers: _headers(authToken: authToken), + body: json.encode(body), + ); + return response; +} + +Future getRequest(String service, String url, {String? authToken}) async { + final response = await http.get(_uri(service, url), + headers: _headers(authToken: authToken), + ); + return response; +} + +Future putRequest(String service, String url, Map body, {String? authToken}) async { + final response = await http.put(_uri(service, url), + headers: _headers(authToken: authToken), + body: json.encode(body), + ); + return response; +} + +Future patchRequest(String service, String url, Map body, {String? authToken}) async { + final response = await http.patch(_uri(service, url), + headers: _headers(authToken: authToken), + body: json.encode(body), + ); + return response; +} + +Future deleteRequest(String service, String url, Map body, {String? authToken}) async { + final response = await http.delete(_uri(service, url), + headers: _headers(authToken: authToken), + body: json.encode(body), + ); + return response; +} + +Future _fileUploadRequest(String service, String url, String fileName, String fileType, String mediaType, List bytes, {String? authToken}) async { + var request = http.MultipartRequest('POST', _uri(service, url)); + + var multipartFile = http.MultipartFile.fromBytes( + fileType, + bytes, + contentType: MediaType.parse(mediaType), + filename: fileName, + ); + + request.files.add(multipartFile); + + if (authToken != null && authToken.isNotEmpty) { + request.headers['Authorization'] = 'Bearer $authToken'; + } + + return request.send(); +} + +void _throwConnectivityError(http.Response response, Object e) { + throw ConnectivityError( + code: response.statusCode, + message: e is FormatException + ? 'Invalid response format. error: ${e.toString()}' + : 'Unknown error occurred, error: ${e.toString()}', + ); +} + +Future> _handleResponse(Future r) async { + late http.Response response; + try { + response = await r; + } catch(e) { + throw ConnectivityError(message: e.toString()); + } + + late HTTPMessage message; + try { + message = HTTPMessage.fromJson(json.decode(response.body)); + } catch(e) { + _throwConnectivityError(response, e); + } + + if (response.statusCode < 200 || response.statusCode >= 300) { + late ErrorResponse error; + try { + error = ErrorResponse.fromJson(message.data); + } catch(e) { + _throwConnectivityError(response, e); + } + throw error; + } + + return message.data; +} + +Future> getPOSTResponse(String service, String url, Map body, {String? authToken}) async { + return _handleResponse(postRequest(service, url, body, authToken: authToken)); +} + +Future> getGETResponse(String service, String url, {String? authToken}) async { + return _handleResponse(getRequest(service, url, authToken: authToken)); +} + +Future> getPUTResponse(String service, String url, Map body, {String? authToken}) async { + return _handleResponse(putRequest(service, url, body, authToken: authToken)); +} + +Future> getPATCHResponse(String service, String url, Map body, {String? authToken}) async { + return _handleResponse(patchRequest(service, url, body, authToken: authToken)); +} + +Future> getDELETEResponse(String service, String url, Map body, {String? authToken}) async { +return _handleResponse(deleteRequest(service, url, body, authToken: authToken)); +} + +Future getFileUploadResponse(String service, String url, String fileName, String fileType, String mediaType, List bytes, {String? authToken}) async { + final streamedResponse = await _fileUploadRequest(service, url, fileName, fileType, mediaType, bytes, authToken: authToken); + return FileUploaded.fromJson(await _handleResponse(http.Response.fromStream(streamedResponse))); +} diff --git a/frontend/pshared/lib/utils/image/conversion.dart b/frontend/pshared/lib/utils/image/conversion.dart new file mode 100644 index 0000000..09da826 --- /dev/null +++ b/frontend/pshared/lib/utils/image/conversion.dart @@ -0,0 +1,45 @@ +import 'package:image/image.dart' as img; + +import 'package:share_plus/share_plus.dart'; + +import 'package:pshared/api/errors/failed_to_read_image.dart'; +import 'package:pshared/config/constants.dart'; +import 'package:pshared/utils/image/transformed.dart'; + + +bool _imageTooBig(img.Image image, int maxDimension) { + return image.width > maxDimension && image.height > maxDimension; +} + +bool _imageTooSmall(img.Image image, int maxDimension) { + return image.width < maxDimension && image.height < maxDimension; +} + +TransformedImage _getImageBytes(img.Image image, int maxDimension) { + List imageBytes; + + if (_imageTooBig(image, maxDimension) || _imageTooSmall(image, maxDimension)) { + final double scale = image.width < image.height + ? maxDimension / image.width + : maxDimension / image.height; + + int newWidth = (image.width * scale).toInt(); + int newHeight = (image.height * scale).toInt(); + + final img.Image resizedImage = img.copyResize(image, width: newWidth, height: newHeight); + image = resizedImage; + } + + imageBytes = img.encodePng(image); + return TransformedImage(imageBytes, 'image/png'); +} + +Future defaultTransformImage(XFile file, {int? maxDimension}) async { + maxDimension = maxDimension ?? Constants.defaultDimensionLength; + img.Image? image = img.decodeImage(await file.readAsBytes()); + if (image == null) { + throw ErrorFailedToReadImage(); + } + + return _getImageBytes(image, maxDimension); +} diff --git a/frontend/pshared/lib/utils/image/transformed.dart b/frontend/pshared/lib/utils/image/transformed.dart new file mode 100644 index 0000000..5fde24f --- /dev/null +++ b/frontend/pshared/lib/utils/image/transformed.dart @@ -0,0 +1,6 @@ +class TransformedImage { + final List bytes; + final String imageType; + + TransformedImage(this.bytes, this.imageType); +} diff --git a/frontend/pshared/lib/utils/localization.dart b/frontend/pshared/lib/utils/localization.dart new file mode 100644 index 0000000..953df05 --- /dev/null +++ b/frontend/pshared/lib/utils/localization.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; + +import 'package:intl/intl.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/config/constants.dart'; +import 'package:pshared/models/settings/localizations.dart' as loc; +import 'package:pshared/provider/locale.dart'; + + +String currentLocale(BuildContext context) => Provider.of(context, listen: false).locale.languageCode; + +String _localizedString( + BuildContext context, + String Function(loc.Localizations locs, String locale, {String? fallback}) localizationFunction, + loc.Localizations locs, { + String? fallback, +}) => localizationFunction(locs, currentLocale(context), fallback: fallback); + +String _anyLocalizedString( + BuildContext context, + String Function(loc.Localizations locs, String locale, {String? fallback}) localizationFunction, + loc.Localizations locs, { + String? fallback, +}) => localizationFunction( + locs, + currentLocale(context), + fallback: localizationFunction( + locs, + Constants.defaultLocale.languageCode, + fallback: locs.isEmpty + ? fallback + : localizationFunction( + locs, + locs.keys.first, + fallback: fallback, + ), + ), +); + +String name(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _localizedString(context, loc.Localization.name, locs, fallback: fallback); +} + +String anyName(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.name, locs, fallback: fallback); +} + +String hint(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _localizedString(context, loc.Localization.hint, locs, fallback: fallback); +} + +String anyHint(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.hint, locs, fallback: fallback); +} + +String link(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _localizedString(context, loc.Localization.link, locs, fallback: fallback); +} + +String anyLink(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.link, locs, fallback: fallback); +} + +String error(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _localizedString(context, loc.Localization.error, locs, fallback: fallback); +} + +String anyError(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.error, locs, fallback: fallback); +} + +String address(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _localizedString(context, loc.Localization.address, locs, fallback: fallback); +} + +String anyAddress(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.address, locs, fallback: fallback); +} + +String details(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _localizedString(context, loc.Localization.details, locs, fallback: fallback); +} + +String anyDetails(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.details, locs, fallback: fallback); +} + +String route(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _localizedString(context, loc.Localization.route, locs, fallback: fallback); +} + +String anyRoute(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.route, locs, fallback: fallback); +} + +String anyLocationName(BuildContext context, loc.Localizations locs, {String? fallback}) { + return _anyLocalizedString(context, loc.Localization.locationName, locs, fallback: fallback); +} + +String translate(BuildContext context, loc.Localizations locs, String key, {String? fallback}) { + return loc.Localization.translate(locs, currentLocale(context), key, fallback: fallback); +} + + +String anyTranslateFromLang(loc.Localizations locs, String lang, String key, {String? fallback}) => + loc.Localization.translate(locs, lang, key, + fallback: loc.Localization.translate(locs, Constants.defaultLocale.languageCode, key, + fallback: loc.Localization.translate(locs, locs.keys.first, key, fallback: fallback), + ), + ); + +String anyTranslate(BuildContext context, loc.Localizations locs, String key, {String? fallback}) => + anyTranslateFromLang(locs, currentLocale(context), key, fallback: fallback); + +String dateToLocalFormat(BuildContext context, DateTime dateTime) { + String locale = Provider.of(context, listen: false).locale.toString(); + final dateFormat = DateFormat('E, ', locale).add_yMd(); + return dateFormat.format(dateTime); +} + +String dateTimeToLocalFormat(BuildContext context, DateTime dateTime) { + String locale = Provider.of(context, listen: false).locale.toString(); + final dateFormat = DateFormat('E, ', locale).add_yMd().add_jm(); + return dateFormat.format(dateTime); +} + +String dateTimeToLocalFormatAuto(BuildContext context, DateTime dateTime, bool? dateOnly) { + return (dateOnly ?? false) ? dateToLocalFormat(context, dateTime) : dateTimeToLocalFormat(context, dateTime); +} + +String dateTimeOrNullToLocalFormat(BuildContext context, DateTime? dateTime, {String? fallback}) { + if (dateTime == null) return fallback ?? ''; + return dateTimeToLocalFormat(context, dateTime); +} diff --git a/frontend/pshared/lib/utils/name_initials.dart b/frontend/pshared/lib/utils/name_initials.dart new file mode 100644 index 0000000..cc92e21 --- /dev/null +++ b/frontend/pshared/lib/utils/name_initials.dart @@ -0,0 +1,17 @@ +class NameInitials { + + static const unknown = '?'; + +} + + +String getNameInitials(String name) { + if (name.isEmpty) return NameInitials.unknown; + // Split the name by whitespace. + final words = name.trim().split(RegExp(r'\s+')); + if (words.isEmpty) return NameInitials.unknown; + // If there's only one word, return its first letter. + if (words.length == 1) return words.first[0].toUpperCase(); + // Otherwise, use the first letter of the first and last words. + return (words.first[0] + words.last[0]).toUpperCase(); +} \ No newline at end of file diff --git a/frontend/pshared/lib/utils/share.dart b/frontend/pshared/lib/utils/share.dart new file mode 100644 index 0000000..f269fe8 --- /dev/null +++ b/frontend/pshared/lib/utils/share.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + + +Rect? sharePositionOrigin(BuildContext context) { + final RenderBox box = context.findRenderObject() as RenderBox; + return box.localToGlobal(Offset.zero) & box.size; +} \ No newline at end of file diff --git a/frontend/pshared/lib/utils/snackbar.dart b/frontend/pshared/lib/utils/snackbar.dart new file mode 100644 index 0000000..ce2dd3b --- /dev/null +++ b/frontend/pshared/lib/utils/snackbar.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + + +ScaffoldFeatureController notifyUserX(ScaffoldMessengerState sm, String message, { int delaySeconds = 3 }) +{ + return sm.showSnackBar( + SnackBar( + content: Text(message), + duration: Duration(seconds: delaySeconds), + ), + ); +} + +ScaffoldFeatureController notifyUser(BuildContext context, String message, { int delaySeconds = 3 }) { + return notifyUserX(ScaffoldMessenger.of(context), message, delaySeconds: delaySeconds); +} + +Future> postNotifyUser( + BuildContext context, String message, {int delaySeconds = 3}) { + + final completer = Completer>(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final controller = notifyUser(context, message, delaySeconds: delaySeconds); + completer.complete(controller); + }); + + return completer.future; +} diff --git a/frontend/pshared/lib/widgets/locale.dart b/frontend/pshared/lib/widgets/locale.dart new file mode 100644 index 0000000..43ef50d --- /dev/null +++ b/frontend/pshared/lib/widgets/locale.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/locale.dart'; +import 'package:pshared/utils/flagged_locale.dart'; + + +class LocaleChangerDropdown extends StatelessWidget { + final List availableLocales; + final bool lettersMode; + const LocaleChangerDropdown({ + super.key, + required this.availableLocales, + this.lettersMode = false, + }); + + Widget textLabel(BuildContext context, Locale locale) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withAlpha(26), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.language), + Container(width: 4.0), + Text( + locale.languageCode.toUpperCase(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + ), + Icon(Icons.arrow_drop_down, color: Theme.of(context).iconTheme.color), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + if (availableLocales.length <= 1) { + // If only one locale is available, do not show the button. + return const SizedBox.shrink(); + } + + var localeProvider = Provider.of(context); + + return PopupMenuButton( + icon: lettersMode ? textLabel(context, localeProvider.locale) : getCountryFlag(localeProvider.locale), + onSelected: localeProvider.setLocale, + itemBuilder: (BuildContext context) { + return availableLocales.map((Locale locale) { + return PopupMenuItem( + value: locale, + child: lettersMode ? getLocaleNameWidget(locale) : getFlaggedLocale(locale), + ); + }).toList(); + }, + ); + } +} diff --git a/frontend/pshared/lib/widgets/template.dart b/frontend/pshared/lib/widgets/template.dart new file mode 100644 index 0000000..5692f67 --- /dev/null +++ b/frontend/pshared/lib/widgets/template.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/template.dart'; + + +class ResourceContainer extends StatelessWidget { + final Widget Function(BuildContext context, T provider) builder; + final Widget? loading; + final Widget? error; + final Widget? empty; + + const ResourceContainer({ + required this.builder, + this.loading, + this.error, + this.empty, + }); + + @override + Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { + if (provider.isLoading) return loading ?? Center(child: CircularProgressIndicator()); + if (provider.error != null) return error ?? Text('Error while loading data. Try again'); //TODO: need to implement localizations and add more details to the error + if (provider.isEmpty) return empty ?? Text('Empty data'); //TODO: need to implement localizations too + return builder(context, provider); + }); +} diff --git a/frontend/pshared/pubspec.yaml b/frontend/pshared/pubspec.yaml new file mode 100644 index 0000000..4995137 --- /dev/null +++ b/frontend/pshared/pubspec.yaml @@ -0,0 +1,43 @@ +name: pshared +description: A starting point for Dart libraries or applications. +version: 1.0.0 + +environment: + sdk: ^3.1.5 + +# Add regular dependencies here. +dependencies: + json_annotation: ^4.9.0 + http: ^1.1.0 + provider: ^6.0.5 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.20.2 + country_flags: ^3.0.0 + font_awesome_flutter: ^10.5.0 + flutter_svg: ^2.0.9 + http_parser: ^4.0.2 + collection: ^1.18.0 + cached_network_image: ^3.3.0 + jovial_svg: ^1.1.23 + logging: ^1.3.0 + share_plus: ^11.0.0 + uuid: ^4.5.1 + image: ^4.5.4 + shared_preferences: ^2.5.3 + +dev_dependencies: + flutter_lints: ^6.0.0 + lints: ^6.0.0 + test: ^1.21.0 + build_runner: ^2.4.6 + json_serializable: ^6.7.1 + +flutter: + generate: true + + uses-material-design: true + assets: + - assets/flag_of_catalonia.si \ No newline at end of file diff --git a/frontend/pshared/test/test.dart b/frontend/pshared/test/test.dart new file mode 100644 index 0000000..4168ba8 --- /dev/null +++ b/frontend/pshared/test/test.dart @@ -0,0 +1,6 @@ +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + }); +} diff --git a/frontend/pweb/.gitignore b/frontend/pweb/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/frontend/pweb/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/frontend/pweb/.metadata b/frontend/pweb/.metadata new file mode 100644 index 0000000..b95fa4d --- /dev/null +++ b/frontend/pweb/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: web + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/pweb/README.md b/frontend/pweb/README.md new file mode 100644 index 0000000..c43db62 --- /dev/null +++ b/frontend/pweb/README.md @@ -0,0 +1,16 @@ +# web + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/frontend/pweb/analysis_options.yaml b/frontend/pweb/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/frontend/pweb/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/frontend/pweb/android/.gitignore b/frontend/pweb/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/frontend/pweb/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/frontend/pweb/android/app/build.gradle.kts b/frontend/pweb/android/app/build.gradle.kts new file mode 100644 index 0000000..49142dd --- /dev/null +++ b/frontend/pweb/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.web" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.web" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/frontend/pweb/android/app/src/debug/AndroidManifest.xml b/frontend/pweb/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/frontend/pweb/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/pweb/android/app/src/main/AndroidManifest.xml b/frontend/pweb/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1160736 --- /dev/null +++ b/frontend/pweb/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/pweb/android/app/src/main/kotlin/com/example/web/MainActivity.kt b/frontend/pweb/android/app/src/main/kotlin/com/example/web/MainActivity.kt new file mode 100644 index 0000000..b34bbbb --- /dev/null +++ b/frontend/pweb/android/app/src/main/kotlin/com/example/web/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.web + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/frontend/pweb/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/pweb/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/frontend/pweb/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/pweb/android/app/src/main/res/drawable/launch_background.xml b/frontend/pweb/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/frontend/pweb/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/pweb/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/pweb/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..23fa0d8c4e3ff10be5656e2d7ef581600f32f1d5 GIT binary patch literal 2588 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 KMNUMnLSTa8Cq@bY literal 0 HcmV?d00001 diff --git a/frontend/pweb/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/pweb/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e6d8690bca5ca9b9f810668b9405f9ec82070561 GIT binary patch literal 3534 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 IM6N<$f}7UDCjbBd literal 0 HcmV?d00001 diff --git a/frontend/pweb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/pweb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..7059c6dac62a0f16f1a8de15f7780795a53f7827 GIT binary patch literal 5255 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 N002ovPDHLkV1kr=6tMsR literal 0 HcmV?d00001 diff --git a/frontend/pweb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/pweb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..9d7cfe6597771fd7e99827897c63cbce37dea4f6 GIT binary patch literal 7074 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 + + + + + + diff --git a/frontend/pweb/android/app/src/main/res/values/styles.xml b/frontend/pweb/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/frontend/pweb/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/pweb/android/app/src/profile/AndroidManifest.xml b/frontend/pweb/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/frontend/pweb/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/pweb/android/build.gradle.kts b/frontend/pweb/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/frontend/pweb/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/frontend/pweb/android/gradle.properties b/frontend/pweb/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/frontend/pweb/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/frontend/pweb/android/gradle/wrapper/gradle-wrapper.properties b/frontend/pweb/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/frontend/pweb/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/frontend/pweb/android/settings.gradle.kts b/frontend/pweb/android/settings.gradle.kts new file mode 100644 index 0000000..ab39a10 --- /dev/null +++ b/frontend/pweb/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/frontend/pweb/entrypoint.sh b/frontend/pweb/entrypoint.sh new file mode 100755 index 0000000..542be68 --- /dev/null +++ b/frontend/pweb/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +replace_env_var() { + local var_name=$1 + local placeholder="%%$var_name%%" + local value=$(eval echo \$$var_name) + # inject value to the index.html + sed -i "s|$placeholder|$value|g" /usr/share/pweb/index.html +} + +echo "Starting Container" + +replace_env_var "WS_PROTOCOL" +replace_env_var "WS_ENDPOINT" +replace_env_var "API_PROTOCOL" +replace_env_var "SERVICE_HOST" +replace_env_var "API_ENDPOINT" +replace_env_var "AMPLITUDE_SECRET" +replace_env_var "DEFAULT_LOCALE" +replace_env_var "DEFAULT_CURRENCY" + +echo "Passing by launch command" +# Execute the passed command (e.g., starting Nginx) +# exec "$@" +exec nginx -g 'daemon off;' \ No newline at end of file diff --git a/frontend/pweb/ios/.gitignore b/frontend/pweb/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/frontend/pweb/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/frontend/pweb/ios/Flutter/AppFrameworkInfo.plist b/frontend/pweb/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/frontend/pweb/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/frontend/pweb/ios/Flutter/Debug.xcconfig b/frontend/pweb/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/frontend/pweb/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/frontend/pweb/ios/Flutter/Release.xcconfig b/frontend/pweb/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/frontend/pweb/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/frontend/pweb/ios/Podfile b/frontend/pweb/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/frontend/pweb/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/frontend/pweb/ios/Podfile.lock b/frontend/pweb/ios/Podfile.lock new file mode 100644 index 0000000..f9cf8e0 --- /dev/null +++ b/frontend/pweb/ios/Podfile.lock @@ -0,0 +1,61 @@ +PODS: + - Flutter (1.0.0) + - flutter_timezone (0.0.1): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_timezone: + :path: ".symlinks/plugins/flutter_timezone/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + +PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 + +COCOAPODS: 1.16.2 diff --git a/frontend/pweb/ios/Runner.xcodeproj/project.pbxproj b/frontend/pweb/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..7fec881 --- /dev/null +++ b/frontend/pweb/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2C56D73CE91635539A9A15DA /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B53E22FD6A63D40B1C3D0B8 /* Pods_RunnerTests.framework */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7FDB1F7A73965C7C67C9A58A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3191231068A580A1B7EB0FC1 /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 11E3F4B5E7F65D8227C50E6E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3191231068A580A1B7EB0FC1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 61B869663D462553198E2AC1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 6B53E22FD6A63D40B1C3D0B8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 995403D706740B49D3A8D16E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + A6678C3B3551DF40750DA02B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + A6E3941EC81B9DAA2584BE2B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + B5C149D70208E226E8B02D31 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8E6A3AB46FFEEA916FAA437C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2C56D73CE91635539A9A15DA /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FDB1F7A73965C7C67C9A58A /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + F8666F8B24C7533EE74CB803 /* Pods */, + C315FB501BE68ED01CE7B90E /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + C315FB501BE68ED01CE7B90E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3191231068A580A1B7EB0FC1 /* Pods_Runner.framework */, + 6B53E22FD6A63D40B1C3D0B8 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F8666F8B24C7533EE74CB803 /* Pods */ = { + isa = PBXGroup; + children = ( + A6E3941EC81B9DAA2584BE2B /* Pods-Runner.debug.xcconfig */, + 995403D706740B49D3A8D16E /* Pods-Runner.release.xcconfig */, + 11E3F4B5E7F65D8227C50E6E /* Pods-Runner.profile.xcconfig */, + A6678C3B3551DF40750DA02B /* Pods-RunnerTests.debug.xcconfig */, + 61B869663D462553198E2AC1 /* Pods-RunnerTests.release.xcconfig */, + B5C149D70208E226E8B02D31 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + FB52EAF123C25402F515699A /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 8E6A3AB46FFEEA916FAA437C /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 1D3F480F2DA3F801E5B09957 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + CA85DA6319399F521D13FFCD /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1D3F480F2DA3F801E5B09957 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + CA85DA6319399F521D13FFCD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FB52EAF123C25402F515699A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 88GA9VDY6Q; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.web; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A6678C3B3551DF40750DA02B /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.web.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61B869663D462553198E2AC1 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.web.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B5C149D70208E226E8B02D31 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.web.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 88GA9VDY6Q; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.web; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 88GA9VDY6Q; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.web; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/frontend/pweb/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/pweb/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/pweb/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/frontend/pweb/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/pweb/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/pweb/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/frontend/pweb/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/frontend/pweb/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/pweb/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend/pweb/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/pweb/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/pweb/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/frontend/pweb/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/pweb/ios/Runner/AppDelegate.swift b/frontend/pweb/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/frontend/pweb/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..1f38fb1cd5a6c4674c72a727dba6b0f021dfc8c0 GIT binary patch literal 40307 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 P00000NkvXXu0mjfsuoTG literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ef13c47292a4b42f6ab6e3291b0e9110f6c3a054 GIT binary patch literal 1409 zcmV-{1%CR8P)-yglFe7#CQfBY2rntn7Hlnug+g0yZ_jdD>sYJhUfQbPDA{TmX>JJ$kdBCt%}Uu`mK48g145#ZZAr1Rxr-Oi{1xpBo3h8?yB1k6k-Dt=OM<0Y45 zznGc4t@u`FY4P}RKR|`-g`f%{7{xu8zXSjR10oPnd={f9fRq^&s8dN$#uFfwi9k#v z_dACJ!?48#W)llKtrN6u3^0D#EsLgk^6IGrH~L2A?A{x%u*}?-j-e;Uat~!Nw&B#B^ z0!k3z^(%JBTS$XKNeD9H)%|-cT)WKQdmt1rPDR2!3xI8m1&IVfB<%?=LXY+e#uykU zxI=8UVi+9-h)<+o*b=5~MyBw1&Svu%<-*IcQ6zwf01!QB{IQG6L2$!ipA3-2_fI`G-zhw;@J@(DS+6iB*>KnOyL@ZNCYJ10cIHtb}sLRT<-b(cbwql;bC5Y z7_+=Gm@~+E)F?1yfrNpbxTEeLs=pW`)yxZ-RRQX^)L-%)^8;+@IZ&aL&08dw%$oni z$N*qyq^+U3YtJs@Y=%Z2^%(LCKZ)Y

>0!C04lq+MTG(W*PNk9FA{ z69R%e51nkgq4IE#sirC`WqWDz)YTgiB0_o-hB0;-%s-uM(v}yGI`qAdn-|cigE0@> zXy3M_v+ww^-09_q6wAq8S0t3sd%McJY47$6ur+WL()pcC@G(RvM$F+hvA!j zt-6DIovd|~zs*T()ZFb{aVZ{U~e#?GA|joS4F zp|&Ok=#|aUXRL48xURGB+sd4@UHj#dod0c07Hi9mTiRVkrol^>Cu<`pK(G2O_4U8s zd}V<;e&wn}W#JlxM95(NxwgkxeUfjUg%!{v3_8<~KX6ZI*85M-{4M{#VR#5brMorm P00000NkvXXu0mjf7(|~^ literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b805c99ce112233ecb69213af79167a5d6f9893f GIT binary patch literal 2138 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 literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..ef13c47292a4b42f6ab6e3291b0e9110f6c3a054 GIT binary patch literal 1409 zcmV-{1%CR8P)-yglFe7#CQfBY2rntn7Hlnug+g0yZ_jdD>sYJhUfQbPDA{TmX>JJ$kdBCt%}Uu`mK48g145#ZZAr1Rxr-Oi{1xpBo3h8?yB1k6k-Dt=OM<0Y45 zznGc4t@u`FY4P}RKR|`-g`f%{7{xu8zXSjR10oPnd={f9fRq^&s8dN$#uFfwi9k#v z_dACJ!?48#W)llKtrN6u3^0D#EsLgk^6IGrH~L2A?A{x%u*}?-j-e;Uat~!Nw&B#B^ z0!k3z^(%JBTS$XKNeD9H)%|-cT)WKQdmt1rPDR2!3xI8m1&IVfB<%?=LXY+e#uykU zxI=8UVi+9-h)<+o*b=5~MyBw1&Svu%<-*IcQ6zwf01!QB{IQG6L2$!ipA3-2_fI`G-zhw;@J@(DS+6iB*>KnOyL@ZNCYJ10cIHtb}sLRT<-b(cbwql;bC5Y z7_+=Gm@~+E)F?1yfrNpbxTEeLs=pW`)yxZ-RRQX^)L-%)^8;+@IZ&aL&08dw%$oni z$N*qyq^+U3YtJs@Y=%Z2^%(LCKZ)Y

>0!C04lq+MTG(W*PNk9FA{ z69R%e51nkgq4IE#sirC`WqWDz)YTgiB0_o-hB0;-%s-uM(v}yGI`qAdn-|cigE0@> zXy3M_v+ww^-09_q6wAq8S0t3sd%McJY47$6ur+WL()pcC@G(RvM$F+hvA!j zt-6DIovd|~zs*T()ZFb{aVZ{U~e#?GA|joS4F zp|&Ok=#|aUXRL48xURGB+sd4@UHj#dod0c07Hi9mTiRVkrol^>Cu<`pK(G2O_4U8s zd}V<;e&wn}W#JlxM95(NxwgkxeUfjUg%!{v3_8<~KX6ZI*85M-{4M{#VR#5brMorm P00000NkvXXu0mjf7(|~^ literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b80922801396b3904541b906b6cbf0e3961afd GIT binary patch literal 2922 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 Ub_GcDB>(^b07*qoM6N<$f{|r~F#rGn literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f6628045068c87b8ddbfbf7b4ab999ce4ab6d2 GIT binary patch literal 4392 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

&u&B)yEX+_FnEf)BZ^9?2^3)#DrR( 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 wZHuo6NFi@UfToc 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 y#9b{-+p!9w(5=g(Ks!OB9ghj}gU1B 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 N002ovPDHLkV1kr=6tMsR literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..0905fb6f4df6db424067af0bde0daaf76e7d3662 GIT binary patch literal 2785 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 literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend/pweb/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0d3a9036beee0139bff444c42d220a0310d99ca1 GIT binary patch literal 5622 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 Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/frontend/pweb/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/frontend/pweb/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/pweb/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/frontend/pweb/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/pweb/ios/Runner/Base.lproj/Main.storyboard b/frontend/pweb/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/frontend/pweb/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/pweb/ios/Runner/Info.plist b/frontend/pweb/ios/Runner/Info.plist new file mode 100644 index 0000000..8f80ced --- /dev/null +++ b/frontend/pweb/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Web + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + web + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/frontend/pweb/ios/Runner/Runner-Bridging-Header.h b/frontend/pweb/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/frontend/pweb/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/frontend/pweb/ios/RunnerTests/RunnerTests.swift b/frontend/pweb/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/frontend/pweb/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/frontend/pweb/l10n.yaml b/frontend/pweb/l10n.yaml new file mode 100644 index 0000000..da8a858 --- /dev/null +++ b/frontend/pweb/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/l10n +output-dir: lib/generated/i18n +template-arb-file: en.arb +output-localization-file: app_localizations.dart +untranslated-messages-file: untranslated.txt \ No newline at end of file diff --git a/frontend/pweb/lib/app/app.dart b/frontend/pweb/lib/app/app.dart new file mode 100644 index 0000000..f6edcc6 --- /dev/null +++ b/frontend/pweb/lib/app/app.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/config/constants.dart'; +import 'package:pshared/provider/locale.dart'; + +import 'package:pweb/app/router/router.dart'; + +import 'package:pshared/generated/i18n/ps_localizations.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +final _router = createRouter(); + +class PayApp extends StatelessWidget { + const PayApp({super.key}); + + @override + Widget build(BuildContext context) => MaterialApp.router( + title: 'Profee Pay', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Constants.themeColor), + useMaterial3: true, + ), + routerConfig: _router, + localizationsDelegates: [ + ...PSLocalizations.localizationsDelegates, + ...AppLocalizations.localizationsDelegates, + ], + supportedLocales: AppLocalizations.supportedLocales, + locale: context.watch().locale, + ); +} diff --git a/frontend/pweb/lib/app/locale_manager.dart b/frontend/pweb/lib/app/locale_manager.dart new file mode 100644 index 0000000..31804b3 --- /dev/null +++ b/frontend/pweb/lib/app/locale_manager.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import 'package:intl/find_locale.dart'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:pshared/provider/locale.dart'; + + +String _localeVarStorageName() { + return 'mcrm_last_locale'; +} + +Locale _selectDefaultLocale(List appLocales, Locale defaultLocale) { + return appLocales.contains(defaultLocale) + ? defaultLocale + : appLocales.isEmpty + ? throw ArgumentError('empty application locales list', 'appLocales') + : appLocales.first; +} + + +class LocaleManager { + late SharedPreferences _prefs; + final List appLocales; + final Locale _defaultLocale; + final LocaleProvider localeProvider; + + LocaleManager(this.localeProvider, this.appLocales, Locale defaultLocale) + : _defaultLocale = _selectDefaultLocale(appLocales, defaultLocale) { + SharedPreferences.getInstance().then((prefs) { + _prefs = prefs; + _initializeLocaleProvider(); + }); + } + + Future _initializeLocaleProvider() async { + final initialLocale = await _getInitialLocale(); + localeProvider.setLocale(initialLocale); + localeProvider.addListener(_onLocaleChanged); + } + + Future _getInitialLocale() async { + final locale = await _pickLocale(); + return appLocales.contains(locale) ? locale : _defaultLocale; + } + + Future _pickLocale() async { + String? savedLocaleCode = _prefs.getString(_localeVarStorageName()); + if (savedLocaleCode != null) { + return Locale(savedLocaleCode); + } + + String systemLocaleString = await findSystemLocale(); + final List localeParts = systemLocaleString.split('_'); + final Locale systemLocale = Locale(localeParts[0]); + + final res = appLocales.contains(systemLocale); + + return res ? systemLocale : _defaultLocale; + } + + Future saveLocale(Locale locale) async { + return _prefs.setString(_localeVarStorageName(), locale.toString()); + } + + Future _onLocaleChanged() async { + return saveLocale(localeProvider.locale); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/app/router/page_params.dart b/frontend/pweb/lib/app/router/page_params.dart new file mode 100644 index 0000000..e0aab00 --- /dev/null +++ b/frontend/pweb/lib/app/router/page_params.dart @@ -0,0 +1,15 @@ +enum PageParams { + token, + projectRef, + roleRef, + taskRef, + invitationRef, +} + +String routerPageParam(PageParams param) { + return ':${param.name}'; +} + +String routerAddParam(PageParams param) { + return '/${routerPageParam(param)}'; +} \ No newline at end of file diff --git a/frontend/pweb/lib/app/router/pages.dart b/frontend/pweb/lib/app/router/pages.dart new file mode 100644 index 0000000..a7e0005 --- /dev/null +++ b/frontend/pweb/lib/app/router/pages.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +enum Pages { + root, + sfactor, + login, + methods, + verify, + signup, + settings, + dashboard, + profile, + recipients, + users, + roles, + permissions, + invitations, +} + +String routerPath(String page) { + return '/$page'; +} + +String routerPage(Pages page) { + return page == Pages.root ? '/' : routerPath(page.name); +} + +String _pagePath(Pages page, {String? objectRef}) => _pagesPath([page], objectRef: objectRef); + +String _pagesPath(List pages, {String? objectRef}) { + final path = pages.map(routerPage).join(); + return objectRef != null ? '$path/$objectRef' : path; +} + +void navigateAndReplace(BuildContext context, Pages page, {String? objectRef, Object? extra}) { + context.go(_pagePath(page, objectRef: objectRef), extra: extra); +} + +void navigate(BuildContext context, Pages page, {String? objectRef, Object? extra}) { + navigatePages(context, [page], objectRef: objectRef, extra: extra); +} + +void navigatePages(BuildContext context, List pages, {String? objectRef, Object? extra}) { + context.push(_pagesPath(pages, objectRef: objectRef), extra: extra); +} + +void navigateNamed(BuildContext context, Pages page, {String? objectRef, Object? extra}) { + context.pushNamed(page.name, extra: extra); +} + + +void navigateNamedAndReplace(BuildContext context, Pages page, {String? objectRef, Object? extra}) { + context.replaceNamed(page.name, extra: extra); +} + +void navigateNext(BuildContext context, Pages page, {Object? extra}) { + WidgetsBinding.instance.addPostFrameCallback((_) => navigate(context, page, extra: extra)); +} \ No newline at end of file diff --git a/frontend/pweb/lib/app/router/router.dart b/frontend/pweb/lib/app/router/router.dart new file mode 100644 index 0000000..e83df5d --- /dev/null +++ b/frontend/pweb/lib/app/router/router.dart @@ -0,0 +1,57 @@ +import 'package:go_router/go_router.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/pages/2fa/page.dart'; +import 'package:pweb/pages/signup/page.dart'; +import 'package:pweb/widgets/sidebar/page.dart'; +import 'package:pweb/pages/login/page.dart'; +import 'package:pweb/pages/errors/not_found.dart'; + +GoRouter createRouter() => GoRouter( + debugLogDiagnostics: true, + routes: [ + GoRoute( + name: Pages.root.name, + path: routerPage(Pages.root), + builder: (_, __) => const LoginPage(), + routes: [ + GoRoute( + name: Pages.login.name, + path: routerPage(Pages.login), + builder: (_, __) => const LoginPage(), + ), + GoRoute( + name: Pages.dashboard.name, + path: routerPage(Pages.dashboard), + builder: (_, __) => const PageSelector(), + ), + GoRoute( + name: Pages.sfactor.name, + path: routerPage(Pages.sfactor), + builder: (context, state) { + // Определяем откуда пришел пользователь + final isFromSignup = state.uri.queryParameters['from'] == 'signup'; + + return TwoFactorCodePage( + onVerificationSuccess: () { + if (isFromSignup) { + // После регистрации -> на страницу логина + context.goNamed(Pages.login.name); + } else { + // После логина -> на дашборд + context.goNamed(Pages.dashboard.name); + } + }, + ); + }, + ), + GoRoute( + name: Pages.signup.name, + path: routerPage(Pages.signup), + builder: (_, __) => const SignUpPage(), + ), + ], + ), + ], + errorBuilder: (_, __) => const NotFoundPage(), +); diff --git a/frontend/pweb/lib/app/timeago.dart b/frontend/pweb/lib/app/timeago.dart new file mode 100644 index 0000000..f4dfa50 --- /dev/null +++ b/frontend/pweb/lib/app/timeago.dart @@ -0,0 +1,27 @@ +import 'package:timeago/timeago.dart' as timeago; + +import 'package:pweb/generated/i18n/app_localizations.dart'; // Ensure this file exports supportedLocales + +// Mapping of language codes to timeago message classes. +final Map _timeagoLocales = { + 'en': timeago.EnMessages(), + 'ru': timeago.RuMessages(), + 'uk': timeago.UkMessages(), + // Add more mappings as needed. +}; + +/// Initializes timeago using the supported locales from AppLocalisations. +/// Optionally, [defaultLocale] can be set (defaults to 'en'). +void initializeTimeagoLocales({String defaultLocale = 'en'}) { + // Assume AppLocalisations.supportedLocales is a static List + final supportedLocales = AppLocalizations.supportedLocales; + + for (final locale in supportedLocales) { + final languageCode = locale.languageCode; + if (_timeagoLocales.containsKey(languageCode)) { + timeago.setLocaleMessages(languageCode, _timeagoLocales[languageCode]!); + } + } + // Set the default locale. + timeago.setDefaultLocale(defaultLocale); +} diff --git a/frontend/pweb/lib/config/constants.dart b/frontend/pweb/lib/config/constants.dart new file mode 100644 index 0000000..d6b52bc --- /dev/null +++ b/frontend/pweb/lib/config/constants.dart @@ -0,0 +1,10 @@ +class Constants { + static const minPasswordCharacters = 8; +} + +class AppConfig { + static const String appName = String.fromEnvironment( + 'APP_NAME', + defaultValue: 'SendiCo', + ); +} diff --git a/frontend/pweb/lib/generated/i18n/app_localizations.dart b/frontend/pweb/lib/generated/i18n/app_localizations.dart new file mode 100644 index 0000000..91889a2 --- /dev/null +++ b/frontend/pweb/lib/generated/i18n/app_localizations.dart @@ -0,0 +1,1562 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_ru.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'i18n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('ru'), + ]; + + /// No description provided for @login. + /// + /// In en, this message translates to: + /// **'Login'** + String get login; + + /// No description provided for @logout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get logout; + + /// No description provided for @profile. + /// + /// In en, this message translates to: + /// **'Profile'** + String get profile; + + /// No description provided for @signup. + /// + /// In en, this message translates to: + /// **'Sign up'** + String get signup; + + /// No description provided for @username. + /// + /// In en, this message translates to: + /// **'Email'** + String get username; + + /// No description provided for @usernameHint. + /// + /// In en, this message translates to: + /// **'email@example.com'** + String get usernameHint; + + /// No description provided for @usernameErrorInvalid. + /// + /// In en, this message translates to: + /// **'Provide a valid email address'** + String get usernameErrorInvalid; + + /// No description provided for @usernameUnknownTLD. + /// + /// In en, this message translates to: + /// **'Domain .{domain} is not known, please, check it'** + String usernameUnknownTLD(Object domain); + + /// No description provided for @password. + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// No description provided for @confirmPassword. + /// + /// In en, this message translates to: + /// **'Confirm password'** + String get confirmPassword; + + /// No description provided for @passwordValidationRuleDigit. + /// + /// In en, this message translates to: + /// **'has digit'** + String get passwordValidationRuleDigit; + + /// No description provided for @passwordValidationRuleUpperCase. + /// + /// In en, this message translates to: + /// **'has uppercase letter'** + String get passwordValidationRuleUpperCase; + + /// No description provided for @passwordValidationRuleLowerCase. + /// + /// In en, this message translates to: + /// **'has lowercase letter'** + String get passwordValidationRuleLowerCase; + + /// No description provided for @passwordValidationRuleSpecialCharacter. + /// + /// In en, this message translates to: + /// **'has special character letter'** + String get passwordValidationRuleSpecialCharacter; + + /// No description provided for @passwordValidationRuleMinCharacters. + /// + /// In en, this message translates to: + /// **'is {charNum} characters long at least'** + String passwordValidationRuleMinCharacters(Object charNum); + + /// No description provided for @passwordsDoNotMatch. + /// + /// In en, this message translates to: + /// **'Passwords do not match'** + String get passwordsDoNotMatch; + + /// No description provided for @passwordValidationError. + /// + /// In en, this message translates to: + /// **'Check that your password {matchesCriteria}'** + String passwordValidationError(Object matchesCriteria); + + /// No description provided for @notificationError. + /// + /// In en, this message translates to: + /// **'Error occurred: {error}'** + String notificationError(Object error); + + /// No description provided for @loginUserNotFound. + /// + /// In en, this message translates to: + /// **'Account {account} has not been registered in the system'** + String loginUserNotFound(Object account); + + /// No description provided for @loginPasswordIncorrect. + /// + /// In en, this message translates to: + /// **'Authorization failed, please check your password'** + String get loginPasswordIncorrect; + + /// No description provided for @internalErrorOccurred. + /// + /// In en, this message translates to: + /// **'An internal server error occurred: {error}, we already know about it and working hard to fix it'** + String internalErrorOccurred(Object error); + + /// No description provided for @noErrorInformation. + /// + /// In en, this message translates to: + /// **'Some error occurred, but we have not error information. We are already investigating the issue'** + String get noErrorInformation; + + /// No description provided for @yourName. + /// + /// In en, this message translates to: + /// **'Your name'** + String get yourName; + + /// No description provided for @nameHint. + /// + /// In en, this message translates to: + /// **'John Doe'** + String get nameHint; + + /// No description provided for @errorPageNotFoundTitle. + /// + /// In en, this message translates to: + /// **'Page Not Found'** + String get errorPageNotFoundTitle; + + /// No description provided for @errorPageNotFoundMessage. + /// + /// In en, this message translates to: + /// **'Oops! We couldn\'t find that page.'** + String get errorPageNotFoundMessage; + + /// No description provided for @errorPageNotFoundHint. + /// + /// In en, this message translates to: + /// **'The page you\'re looking for doesn\'t exist or has been moved. Please check the URL or return to the home page.'** + String get errorPageNotFoundHint; + + /// No description provided for @errorUnknown. + /// + /// In en, this message translates to: + /// **'Unknown error occurred'** + String get errorUnknown; + + /// No description provided for @unknown. + /// + /// In en, this message translates to: + /// **'unknown'** + String get unknown; + + /// No description provided for @goToLogin. + /// + /// In en, this message translates to: + /// **'Go to Login'** + String get goToLogin; + + /// No description provided for @goBack. + /// + /// In en, this message translates to: + /// **'Go Back'** + String get goBack; + + /// No description provided for @goToMainPage. + /// + /// In en, this message translates to: + /// **'Go to Main Page'** + String get goToMainPage; + + /// No description provided for @goToSignUp. + /// + /// In en, this message translates to: + /// **'Go to Sign Up'** + String get goToSignUp; + + /// No description provided for @signupError. + /// + /// In en, this message translates to: + /// **'Failed to signup: {error}'** + String signupError(Object error); + + /// No description provided for @signupSuccess. + /// + /// In en, this message translates to: + /// **'Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.'** + String signupSuccess(Object email); + + /// No description provided for @connectivityError. + /// + /// In en, this message translates to: + /// **'Cannot reach the server at {serverAddress}. Check your network and try again.'** + String connectivityError(Object serverAddress); + + /// No description provided for @errorAccountExists. + /// + /// In en, this message translates to: + /// **'Account already exists'** + String get errorAccountExists; + + /// No description provided for @errorAccountNotVerified. + /// + /// In en, this message translates to: + /// **'Your account hasn\'t been verified yet. Please check your email to complete the verification'** + String get errorAccountNotVerified; + + /// No description provided for @errorLoginUnauthorized. + /// + /// In en, this message translates to: + /// **'Login or password is incorrect. Please try again'** + String get errorLoginUnauthorized; + + /// No description provided for @errorInternalError. + /// + /// In en, this message translates to: + /// **'An internal error occurred. We\'re aware of the issue and working to resolve it. Please try again later'** + String get errorInternalError; + + /// No description provided for @errorVerificationTokenNotFound. + /// + /// In en, this message translates to: + /// **'Account for verification not found. Sign up again'** + String get errorVerificationTokenNotFound; + + /// No description provided for @created. + /// + /// In en, this message translates to: + /// **'Created'** + String get created; + + /// No description provided for @edited. + /// + /// In en, this message translates to: + /// **'Edited'** + String get edited; + + /// No description provided for @errorDataConflict. + /// + /// In en, this message translates to: + /// **'We can’t process your data because it has conflicting or contradictory information.'** + String get errorDataConflict; + + /// No description provided for @errorAccessDenied. + /// + /// In en, this message translates to: + /// **'You do not have permission to access this resource. If you need access, please contact an administrator.'** + String get errorAccessDenied; + + /// No description provided for @errorBrokenPayload. + /// + /// In en, this message translates to: + /// **'The data you sent is invalid or incomplete. Please check your submission and try again.'** + String get errorBrokenPayload; + + /// No description provided for @errorInvalidArgument. + /// + /// In en, this message translates to: + /// **'One or more arguments are invalid. Verify your input and try again.'** + String get errorInvalidArgument; + + /// No description provided for @errorBrokenReference. + /// + /// In en, this message translates to: + /// **'The resource you\'re trying to access could not be referenced. It may have been moved or deleted.'** + String get errorBrokenReference; + + /// No description provided for @errorInvalidQueryParameter. + /// + /// In en, this message translates to: + /// **'One or more query parameters are missing or incorrect. Check them and try again.'** + String get errorInvalidQueryParameter; + + /// No description provided for @errorNotImplemented. + /// + /// In en, this message translates to: + /// **'This feature is not yet available. Please try again later or contact support.'** + String get errorNotImplemented; + + /// No description provided for @errorLicenseRequired. + /// + /// In en, this message translates to: + /// **'A valid license is required to perform this action. Please contact your administrator.'** + String get errorLicenseRequired; + + /// No description provided for @errorNotFound. + /// + /// In en, this message translates to: + /// **'We couldn\'t find the resource you requested. It may have been removed or is temporarily unavailable.'** + String get errorNotFound; + + /// No description provided for @errorNameMissing. + /// + /// In en, this message translates to: + /// **'Please provide a name before continuing.'** + String get errorNameMissing; + + /// No description provided for @errorEmailMissing. + /// + /// In en, this message translates to: + /// **'Please provide an email address before continuing.'** + String get errorEmailMissing; + + /// No description provided for @errorPasswordMissing. + /// + /// In en, this message translates to: + /// **'Please provide a password before continuing.'** + String get errorPasswordMissing; + + /// No description provided for @errorEmailNotRegistered. + /// + /// In en, this message translates to: + /// **'We could not find an account associated with that email address.'** + String get errorEmailNotRegistered; + + /// No description provided for @errorDuplicateEmail. + /// + /// In en, this message translates to: + /// **'This email address is already in use. Try another one or reset your password.'** + String get errorDuplicateEmail; + + /// No description provided for @showDetailsAction. + /// + /// In en, this message translates to: + /// **'Show Details'** + String get showDetailsAction; + + /// No description provided for @errorLogin. + /// + /// In en, this message translates to: + /// **'Error logging in'** + String get errorLogin; + + /// Error message displayed when invitation creation fails + /// + /// In en, this message translates to: + /// **'Failed to create invitaiton'** + String get errorCreatingInvitation; + + /// No description provided for @footerCompanyName. + /// + /// In en, this message translates to: + /// **'Sibilla Solutions LTD'** + String get footerCompanyName; + + /// No description provided for @footerAddress. + /// + /// In en, this message translates to: + /// **'27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'** + String get footerAddress; + + /// No description provided for @footerSupport. + /// + /// In en, this message translates to: + /// **'Support'** + String get footerSupport; + + /// No description provided for @footerEmail. + /// + /// In en, this message translates to: + /// **'Email TBD'** + String get footerEmail; + + /// No description provided for @footerPhoneLabel. + /// + /// In en, this message translates to: + /// **'Phone'** + String get footerPhoneLabel; + + /// No description provided for @footerPhone. + /// + /// In en, this message translates to: + /// **'+357 22 000 253'** + String get footerPhone; + + /// No description provided for @footerTermsOfService. + /// + /// In en, this message translates to: + /// **'Terms of Service'** + String get footerTermsOfService; + + /// No description provided for @footerPrivacyPolicy. + /// + /// In en, this message translates to: + /// **'Privacy Policy'** + String get footerPrivacyPolicy; + + /// No description provided for @footerCookiePolicy. + /// + /// In en, this message translates to: + /// **'Cookie Policy'** + String get footerCookiePolicy; + + /// No description provided for @navigationLogout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get navigationLogout; + + /// No description provided for @dashboard. + /// + /// In en, this message translates to: + /// **'Dashboard'** + String get dashboard; + + /// No description provided for @navigationUsersSettings. + /// + /// In en, this message translates to: + /// **'Users'** + String get navigationUsersSettings; + + /// No description provided for @navigationRolesSettings. + /// + /// In en, this message translates to: + /// **'Roles'** + String get navigationRolesSettings; + + /// No description provided for @navigationPermissionsSettings. + /// + /// In en, this message translates to: + /// **'Permissions'** + String get navigationPermissionsSettings; + + /// No description provided for @usersManagement. + /// + /// In en, this message translates to: + /// **'User Management'** + String get usersManagement; + + /// No description provided for @navigationOrganizationSettings. + /// + /// In en, this message translates to: + /// **'Organization settings'** + String get navigationOrganizationSettings; + + /// No description provided for @navigationAccountSettings. + /// + /// In en, this message translates to: + /// **'Profile settings'** + String get navigationAccountSettings; + + /// No description provided for @twoFactorPrompt. + /// + /// In en, this message translates to: + /// **'Enter the 6-digit code we sent to your device'** + String get twoFactorPrompt; + + /// No description provided for @twoFactorResend. + /// + /// In en, this message translates to: + /// **'Didn’t receive a code? Resend'** + String get twoFactorResend; + + /// No description provided for @twoFactorTitle. + /// + /// In en, this message translates to: + /// **'Two-Factor Authentication'** + String get twoFactorTitle; + + /// No description provided for @twoFactorError. + /// + /// In en, this message translates to: + /// **'Invalid code. Please try again.'** + String get twoFactorError; + + /// No description provided for @payoutNavDashboard. + /// + /// In en, this message translates to: + /// **'Dashboard'** + String get payoutNavDashboard; + + /// No description provided for @payoutNavSendPayout. + /// + /// In en, this message translates to: + /// **'Send payout'** + String get payoutNavSendPayout; + + /// No description provided for @payoutNavRecipients. + /// + /// In en, this message translates to: + /// **'Recipients'** + String get payoutNavRecipients; + + /// No description provided for @payoutNavReports. + /// + /// In en, this message translates to: + /// **'Reports'** + String get payoutNavReports; + + /// No description provided for @payoutNavSettings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get payoutNavSettings; + + /// No description provided for @payoutNavLogout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get payoutNavLogout; + + /// No description provided for @payoutNavMethods. + /// + /// In en, this message translates to: + /// **'Payouts'** + String get payoutNavMethods; + + /// No description provided for @expand. + /// + /// In en, this message translates to: + /// **'Expand'** + String get expand; + + /// No description provided for @collapse. + /// + /// In en, this message translates to: + /// **'Collapse'** + String get collapse; + + /// Title of the recipient address book page + /// + /// In en, this message translates to: + /// **'Recipient address book'** + String get pageTitleRecipients; + + /// Tooltip and button label to add a new recipient + /// + /// In en, this message translates to: + /// **'Add new'** + String get actionAddNew; + + /// Column header for who manages the payout data + /// + /// In en, this message translates to: + /// **'Data owner'** + String get colDataOwner; + + /// Column header for recipient avatar + /// + /// In en, this message translates to: + /// **'Avatar'** + String get colAvatar; + + /// Column header for recipient name + /// + /// In en, this message translates to: + /// **'Name'** + String get colName; + + /// Column header for recipient email address + /// + /// In en, this message translates to: + /// **'Email'** + String get colEmail; + + /// Column header for payout readiness status + /// + /// In en, this message translates to: + /// **'Status'** + String get colStatus; + + /// Status indicating payouts can be sent immediately + /// + /// In en, this message translates to: + /// **'Ready'** + String get statusReady; + + /// Status indicating recipient is registered but not yet fully ready + /// + /// In en, this message translates to: + /// **'Registered'** + String get statusRegistered; + + /// Status indicating recipient has not completed registration + /// + /// In en, this message translates to: + /// **'Not registered'** + String get statusNotRegistered; + + /// Label for recipients whose payout data is managed internally by the user/company + /// + /// In en, this message translates to: + /// **'Managed by me'** + String get typeInternal; + + /// Label for recipients who manage their own payout data + /// + /// In en, this message translates to: + /// **'Self‑managed'** + String get typeExternal; + + /// No description provided for @searchHint. + /// + /// In en, this message translates to: + /// **'Search recipients'** + String get searchHint; + + /// No description provided for @colActions. + /// + /// In en, this message translates to: + /// **'Actions'** + String get colActions; + + /// No description provided for @menuEdit. + /// + /// In en, this message translates to: + /// **'Edit'** + String get menuEdit; + + /// No description provided for @menuSendPayout. + /// + /// In en, this message translates to: + /// **'Send payout'** + String get menuSendPayout; + + /// No description provided for @tooltipRowActions. + /// + /// In en, this message translates to: + /// **'More actions'** + String get tooltipRowActions; + + /// No description provided for @accountSettings. + /// + /// In en, this message translates to: + /// **'Account Settings'** + String get accountSettings; + + /// No description provided for @accountNameUpdateError. + /// + /// In en, this message translates to: + /// **'Failed to update account name'** + String get accountNameUpdateError; + + /// No description provided for @settingsSuccessfullyUpdated. + /// + /// In en, this message translates to: + /// **'Settings successfully updated'** + String get settingsSuccessfullyUpdated; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + + /// No description provided for @failedToUpdateLanguage. + /// + /// In en, this message translates to: + /// **'Failed to update language'** + String get failedToUpdateLanguage; + + /// No description provided for @settingsImageUpdateError. + /// + /// In en, this message translates to: + /// **'Couldn\'t update the image'** + String get settingsImageUpdateError; + + /// No description provided for @settingsImageTitle. + /// + /// In en, this message translates to: + /// **'Image'** + String get settingsImageTitle; + + /// No description provided for @settingsImageHint. + /// + /// In en, this message translates to: + /// **'Tap to change the image'** + String get settingsImageHint; + + /// No description provided for @accountName. + /// + /// In en, this message translates to: + /// **'Name'** + String get accountName; + + /// No description provided for @accountNameHint. + /// + /// In en, this message translates to: + /// **'Specify your name'** + String get accountNameHint; + + /// No description provided for @avatar. + /// + /// In en, this message translates to: + /// **'Profile photo'** + String get avatar; + + /// No description provided for @avatarHint. + /// + /// In en, this message translates to: + /// **'Tap to update'** + String get avatarHint; + + /// No description provided for @avatarUpdateError. + /// + /// In en, this message translates to: + /// **'Failed to update profile photo'** + String get avatarUpdateError; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @notSet. + /// + /// In en, this message translates to: + /// **'not set'** + String get notSet; + + /// No description provided for @search. + /// + /// In en, this message translates to: + /// **'Search...'** + String get search; + + /// No description provided for @ok. + /// + /// In en, this message translates to: + /// **'Ok'** + String get ok; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @confirm. + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirm; + + /// No description provided for @back. + /// + /// In en, this message translates to: + /// **'Back'** + String get back; + + /// Title of the operation history page + /// + /// In en, this message translates to: + /// **'Operation history'** + String get operationfryTitle; + + /// Label for the filters expansion panel + /// + /// In en, this message translates to: + /// **'Filters'** + String get filters; + + /// Label for the date‐range filter + /// + /// In en, this message translates to: + /// **'Period'** + String get period; + + /// Placeholder when no period is selected + /// + /// In en, this message translates to: + /// **'Select period'** + String get selectPeriod; + + /// Button text to apply the filters + /// + /// In en, this message translates to: + /// **'Apply'** + String get apply; + + /// Template for a single status filter chip + /// + /// In en, this message translates to: + /// **'{status}'** + String status(String status); + + /// Status indicating the operation succeeded + /// + /// In en, this message translates to: + /// **'Successful'** + String get operationStatusSuccessful; + + /// Status indicating the operation is pending + /// + /// In en, this message translates to: + /// **'Pending'** + String get operationStatusPending; + + /// Status indicating the operation failed + /// + /// In en, this message translates to: + /// **'Unsuccessful'** + String get operationStatusUnsuccessful; + + /// Table column header for status + /// + /// In en, this message translates to: + /// **'Status'** + String get statusColumn; + + /// Table column header for file name + /// + /// In en, this message translates to: + /// **'File name'** + String get fileNameColumn; + + /// Table column header for the original amount + /// + /// In en, this message translates to: + /// **'Amount'** + String get amountColumn; + + /// Table column header for the converted amount + /// + /// In en, this message translates to: + /// **'To amount'** + String get toAmountColumn; + + /// Table column header for the payment ID + /// + /// In en, this message translates to: + /// **'Pay ID'** + String get payIdColumn; + + /// Table column header for the masked card number + /// + /// In en, this message translates to: + /// **'Card number'** + String get cardNumberColumn; + + /// Table column header for recipient name + /// + /// In en, this message translates to: + /// **'Name'** + String get nameColumn; + + /// Table column header for the date/time + /// + /// In en, this message translates to: + /// **'Date'** + String get dateColumn; + + /// Table column header for any comment + /// + /// In en, this message translates to: + /// **'Comment'** + String get commentColumn; + + /// No description provided for @paymentConfigTitle. + /// + /// In en, this message translates to: + /// **'Where to receive money'** + String get paymentConfigTitle; + + /// No description provided for @paymentConfigSubtitle. + /// + /// In en, this message translates to: + /// **'Add multiple methods and choose your primary one.'** + String get paymentConfigSubtitle; + + /// No description provided for @addPaymentMethod. + /// + /// In en, this message translates to: + /// **'Add payment method'** + String get addPaymentMethod; + + /// No description provided for @makeMain. + /// + /// In en, this message translates to: + /// **'Make primary'** + String get makeMain; + + /// No description provided for @advanced. + /// + /// In en, this message translates to: + /// **'Advanced'** + String get advanced; + + /// No description provided for @fallbackExplanation. + /// + /// In en, this message translates to: + /// **'If the primary method is unavailable, we will try the next enabled one in the list.'** + String get fallbackExplanation; + + /// Button label to delete a payment method + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// Confirmation dialog message shown before a payment method is removed + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this payment method?'** + String get deletePaymentConfirmation; + + /// Button label to edit a payment method + /// + /// In en, this message translates to: + /// **'Edit'** + String get edit; + + /// Tooltip for an overflow menu button that reveals extra actions for a payment method + /// + /// In en, this message translates to: + /// **'More actions'** + String get moreActions; + + /// No description provided for @noPayouts. + /// + /// In en, this message translates to: + /// **'No Payouts'** + String get noPayouts; + + /// No description provided for @enterBankName. + /// + /// In en, this message translates to: + /// **'Enter bank name'** + String get enterBankName; + + /// No description provided for @paymentType. + /// + /// In en, this message translates to: + /// **'Payment Method Type'** + String get paymentType; + + /// No description provided for @selectPaymentType. + /// + /// In en, this message translates to: + /// **'Please select a payment method type'** + String get selectPaymentType; + + /// No description provided for @paymentTypeCard. + /// + /// In en, this message translates to: + /// **'Credit Card'** + String get paymentTypeCard; + + /// No description provided for @paymentTypeBankAccount. + /// + /// In en, this message translates to: + /// **'Russian Bank Account'** + String get paymentTypeBankAccount; + + /// No description provided for @paymentTypeIban. + /// + /// In en, this message translates to: + /// **'IBAN'** + String get paymentTypeIban; + + /// No description provided for @paymentTypeWallet. + /// + /// In en, this message translates to: + /// **'Wallet'** + String get paymentTypeWallet; + + /// No description provided for @cardNumber. + /// + /// In en, this message translates to: + /// **'Card Number'** + String get cardNumber; + + /// No description provided for @enterCardNumber. + /// + /// In en, this message translates to: + /// **'Enter the card number'** + String get enterCardNumber; + + /// No description provided for @cardholderName. + /// + /// In en, this message translates to: + /// **'Cardholder Name'** + String get cardholderName; + + /// No description provided for @iban. + /// + /// In en, this message translates to: + /// **'IBAN'** + String get iban; + + /// No description provided for @enterIban. + /// + /// In en, this message translates to: + /// **'Enter IBAN'** + String get enterIban; + + /// No description provided for @bic. + /// + /// In en, this message translates to: + /// **'BIC'** + String get bic; + + /// No description provided for @bankName. + /// + /// In en, this message translates to: + /// **'Bank Name'** + String get bankName; + + /// No description provided for @accountHolder. + /// + /// In en, this message translates to: + /// **'Account Holder'** + String get accountHolder; + + /// No description provided for @enterAccountHolder. + /// + /// In en, this message translates to: + /// **'Enter account holder'** + String get enterAccountHolder; + + /// No description provided for @enterBic. + /// + /// In en, this message translates to: + /// **'Enter BIC'** + String get enterBic; + + /// No description provided for @walletId. + /// + /// In en, this message translates to: + /// **'Wallet ID'** + String get walletId; + + /// No description provided for @enterWalletId. + /// + /// In en, this message translates to: + /// **'Enter wallet ID'** + String get enterWalletId; + + /// No description provided for @recipients. + /// + /// In en, this message translates to: + /// **'Recipients'** + String get recipients; + + /// No description provided for @recipientName. + /// + /// In en, this message translates to: + /// **'Recipient Name'** + String get recipientName; + + /// No description provided for @enterRecipientName. + /// + /// In en, this message translates to: + /// **'Enter recipient name'** + String get enterRecipientName; + + /// No description provided for @inn. + /// + /// In en, this message translates to: + /// **'INN'** + String get inn; + + /// No description provided for @enterInn. + /// + /// In en, this message translates to: + /// **'Enter INN'** + String get enterInn; + + /// No description provided for @kpp. + /// + /// In en, this message translates to: + /// **'KPP'** + String get kpp; + + /// No description provided for @enterKpp. + /// + /// In en, this message translates to: + /// **'Enter KPP'** + String get enterKpp; + + /// No description provided for @accountNumber. + /// + /// In en, this message translates to: + /// **'Account Number'** + String get accountNumber; + + /// No description provided for @enterAccountNumber. + /// + /// In en, this message translates to: + /// **'Enter account number'** + String get enterAccountNumber; + + /// No description provided for @correspondentAccount. + /// + /// In en, this message translates to: + /// **'Correspondent Account'** + String get correspondentAccount; + + /// No description provided for @enterCorrespondentAccount. + /// + /// In en, this message translates to: + /// **'Enter correspondent account'** + String get enterCorrespondentAccount; + + /// No description provided for @bik. + /// + /// In en, this message translates to: + /// **'BIK'** + String get bik; + + /// No description provided for @enterBik. + /// + /// In en, this message translates to: + /// **'Enter BIK'** + String get enterBik; + + /// No description provided for @add. + /// + /// In en, this message translates to: + /// **'Add'** + String get add; + + /// No description provided for @expiryDate. + /// + /// In en, this message translates to: + /// **'Expiry (MM/YY)'** + String get expiryDate; + + /// No description provided for @firstName. + /// + /// In en, this message translates to: + /// **'First Name'** + String get firstName; + + /// No description provided for @enterFirstName. + /// + /// In en, this message translates to: + /// **'Enter First Name'** + String get enterFirstName; + + /// No description provided for @lastName. + /// + /// In en, this message translates to: + /// **'Last Name'** + String get lastName; + + /// No description provided for @enterLastName. + /// + /// In en, this message translates to: + /// **'Enter Last Name'** + String get enterLastName; + + /// No description provided for @sendSingle. + /// + /// In en, this message translates to: + /// **'Send single transaction'** + String get sendSingle; + + /// No description provided for @sendMultiple. + /// + /// In en, this message translates to: + /// **'Send multiple transactions'** + String get sendMultiple; + + /// No description provided for @addFunds. + /// + /// In en, this message translates to: + /// **'Add Funds'** + String get addFunds; + + /// No description provided for @close. + /// + /// In en, this message translates to: + /// **'Close'** + String get close; + + /// No description provided for @multiplePayout. + /// + /// In en, this message translates to: + /// **'Multiple Payout'** + String get multiplePayout; + + /// No description provided for @howItWorks. + /// + /// In en, this message translates to: + /// **'How it works?'** + String get howItWorks; + + /// No description provided for @exampleTitle. + /// + /// In en, this message translates to: + /// **'File Format & Sample'** + String get exampleTitle; + + /// No description provided for @downloadSampleCSV. + /// + /// In en, this message translates to: + /// **'Download sample.csv'** + String get downloadSampleCSV; + + /// No description provided for @tokenColumn. + /// + /// In en, this message translates to: + /// **'Token (required)'** + String get tokenColumn; + + /// No description provided for @currency. + /// + /// In en, this message translates to: + /// **'Currency'** + String get currency; + + /// No description provided for @amount. + /// + /// In en, this message translates to: + /// **'Amount'** + String get amount; + + /// No description provided for @comment. + /// + /// In en, this message translates to: + /// **'Comment'** + String get comment; + + /// No description provided for @uploadCSV. + /// + /// In en, this message translates to: + /// **'Upload your CSV'** + String get uploadCSV; + + /// No description provided for @upload. + /// + /// In en, this message translates to: + /// **'Upload'** + String get upload; + + /// No description provided for @hintUpload. + /// + /// In en, this message translates to: + /// **'Supported format: .CSV · Max size 1 MB'** + String get hintUpload; + + /// No description provided for @uploadHistory. + /// + /// In en, this message translates to: + /// **'Upload History'** + String get uploadHistory; + + /// No description provided for @payout. + /// + /// In en, this message translates to: + /// **'Payout'** + String get payout; + + /// No description provided for @sendTo. + /// + /// In en, this message translates to: + /// **'Send Payout To'** + String get sendTo; + + /// No description provided for @send. + /// + /// In en, this message translates to: + /// **'Send Payout'** + String get send; + + /// No description provided for @recipientPaysFee. + /// + /// In en, this message translates to: + /// **'Recipient pays the fee'** + String get recipientPaysFee; + + /// Label showing the amount sent + /// + /// In en, this message translates to: + /// **'Sent amount: \${amount}'** + String sentAmount(String amount); + + /// Label showing the transaction fee + /// + /// In en, this message translates to: + /// **'Fee: \${fee}'** + String fee(String fee); + + /// Label showing how much the recipient will receive + /// + /// In en, this message translates to: + /// **'Recipient will receive: \${amount}'** + String recipientWillReceive(String amount); + + /// Label showing the total amount of the transaction + /// + /// In en, this message translates to: + /// **'Total: \${total}'** + String total(String total); + + /// No description provided for @hideDetails. + /// + /// In en, this message translates to: + /// **'Hide Details'** + String get hideDetails; + + /// No description provided for @showDetails. + /// + /// In en, this message translates to: + /// **'Show Details'** + String get showDetails; + + /// No description provided for @whereGetMoney. + /// + /// In en, this message translates to: + /// **'Source of funds for debit'** + String get whereGetMoney; + + /// No description provided for @details. + /// + /// In en, this message translates to: + /// **'Details'** + String get details; + + /// No description provided for @addRecipient. + /// + /// In en, this message translates to: + /// **'Add Recipient'** + String get addRecipient; + + /// No description provided for @editRecipient. + /// + /// In en, this message translates to: + /// **'Edit Recipient'** + String get editRecipient; + + /// No description provided for @saveRecipient. + /// + /// In en, this message translates to: + /// **'Save Recipient'** + String get saveRecipient; + + /// No description provided for @choosePaymentMethod. + /// + /// In en, this message translates to: + /// **'Payment Methods (choose at least 1)'** + String get choosePaymentMethod; + + /// No description provided for @recipientFormRule. + /// + /// In en, this message translates to: + /// **'Recipient must have at least one payment method'** + String get recipientFormRule; + + /// No description provided for @allStatus. + /// + /// In en, this message translates to: + /// **'All'** + String get allStatus; + + /// No description provided for @readyStatus. + /// + /// In en, this message translates to: + /// **'Ready'** + String get readyStatus; + + /// No description provided for @registeredStatus. + /// + /// In en, this message translates to: + /// **'Registered'** + String get registeredStatus; + + /// No description provided for @notRegisteredStatus. + /// + /// In en, this message translates to: + /// **'Not registered'** + String get notRegisteredStatus; + + /// No description provided for @noRecipientSelected. + /// + /// In en, this message translates to: + /// **'No recipient selected'** + String get noRecipientSelected; + + /// No description provided for @companyName. + /// + /// In en, this message translates to: + /// **'Name of your company'** + String get companyName; + + /// No description provided for @companynameRequired. + /// + /// In en, this message translates to: + /// **'Company name required'** + String get companynameRequired; + + /// No description provided for @errorSignUp. + /// + /// In en, this message translates to: + /// **'Error occured while signing up, try again later'** + String get errorSignUp; + + /// No description provided for @companyDescription. + /// + /// In en, this message translates to: + /// **'Company Description'** + String get companyDescription; + + /// No description provided for @companyDescriptionHint. + /// + /// In en, this message translates to: + /// **'Describe any of the fields of the Company\'s business'** + String get companyDescriptionHint; + + /// No description provided for @optional. + /// + /// In en, this message translates to: + /// **'optional'** + String get optional; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'ru'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'ru': + return AppLocalizationsRu(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/frontend/pweb/lib/generated/i18n/app_localizations_en.dart b/frontend/pweb/lib/generated/i18n/app_localizations_en.dart new file mode 100644 index 0000000..ddcad3b --- /dev/null +++ b/frontend/pweb/lib/generated/i18n/app_localizations_en.dart @@ -0,0 +1,779 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get login => 'Login'; + + @override + String get logout => 'Logout'; + + @override + String get profile => 'Profile'; + + @override + String get signup => 'Sign up'; + + @override + String get username => 'Email'; + + @override + String get usernameHint => 'email@example.com'; + + @override + String get usernameErrorInvalid => 'Provide a valid email address'; + + @override + String usernameUnknownTLD(Object domain) { + return 'Domain .$domain is not known, please, check it'; + } + + @override + String get password => 'Password'; + + @override + String get confirmPassword => 'Confirm password'; + + @override + String get passwordValidationRuleDigit => 'has digit'; + + @override + String get passwordValidationRuleUpperCase => 'has uppercase letter'; + + @override + String get passwordValidationRuleLowerCase => 'has lowercase letter'; + + @override + String get passwordValidationRuleSpecialCharacter => + 'has special character letter'; + + @override + String passwordValidationRuleMinCharacters(Object charNum) { + return 'is $charNum characters long at least'; + } + + @override + String get passwordsDoNotMatch => 'Passwords do not match'; + + @override + String passwordValidationError(Object matchesCriteria) { + return 'Check that your password $matchesCriteria'; + } + + @override + String notificationError(Object error) { + return 'Error occurred: $error'; + } + + @override + String loginUserNotFound(Object account) { + return 'Account $account has not been registered in the system'; + } + + @override + String get loginPasswordIncorrect => + 'Authorization failed, please check your password'; + + @override + String internalErrorOccurred(Object error) { + return 'An internal server error occurred: $error, we already know about it and working hard to fix it'; + } + + @override + String get noErrorInformation => + 'Some error occurred, but we have not error information. We are already investigating the issue'; + + @override + String get yourName => 'Your name'; + + @override + String get nameHint => 'John Doe'; + + @override + String get errorPageNotFoundTitle => 'Page Not Found'; + + @override + String get errorPageNotFoundMessage => 'Oops! We couldn\'t find that page.'; + + @override + String get errorPageNotFoundHint => + 'The page you\'re looking for doesn\'t exist or has been moved. Please check the URL or return to the home page.'; + + @override + String get errorUnknown => 'Unknown error occurred'; + + @override + String get unknown => 'unknown'; + + @override + String get goToLogin => 'Go to Login'; + + @override + String get goBack => 'Go Back'; + + @override + String get goToMainPage => 'Go to Main Page'; + + @override + String get goToSignUp => 'Go to Sign Up'; + + @override + String signupError(Object error) { + return 'Failed to signup: $error'; + } + + @override + String signupSuccess(Object email) { + return 'Email confirmation message has been sent to $email. Please, open it and click link to activate your account.'; + } + + @override + String connectivityError(Object serverAddress) { + return 'Cannot reach the server at $serverAddress. Check your network and try again.'; + } + + @override + String get errorAccountExists => 'Account already exists'; + + @override + String get errorAccountNotVerified => + 'Your account hasn\'t been verified yet. Please check your email to complete the verification'; + + @override + String get errorLoginUnauthorized => + 'Login or password is incorrect. Please try again'; + + @override + String get errorInternalError => + 'An internal error occurred. We\'re aware of the issue and working to resolve it. Please try again later'; + + @override + String get errorVerificationTokenNotFound => + 'Account for verification not found. Sign up again'; + + @override + String get created => 'Created'; + + @override + String get edited => 'Edited'; + + @override + String get errorDataConflict => + 'We can’t process your data because it has conflicting or contradictory information.'; + + @override + String get errorAccessDenied => + 'You do not have permission to access this resource. If you need access, please contact an administrator.'; + + @override + String get errorBrokenPayload => + 'The data you sent is invalid or incomplete. Please check your submission and try again.'; + + @override + String get errorInvalidArgument => + 'One or more arguments are invalid. Verify your input and try again.'; + + @override + String get errorBrokenReference => + 'The resource you\'re trying to access could not be referenced. It may have been moved or deleted.'; + + @override + String get errorInvalidQueryParameter => + 'One or more query parameters are missing or incorrect. Check them and try again.'; + + @override + String get errorNotImplemented => + 'This feature is not yet available. Please try again later or contact support.'; + + @override + String get errorLicenseRequired => + 'A valid license is required to perform this action. Please contact your administrator.'; + + @override + String get errorNotFound => + 'We couldn\'t find the resource you requested. It may have been removed or is temporarily unavailable.'; + + @override + String get errorNameMissing => 'Please provide a name before continuing.'; + + @override + String get errorEmailMissing => + 'Please provide an email address before continuing.'; + + @override + String get errorPasswordMissing => + 'Please provide a password before continuing.'; + + @override + String get errorEmailNotRegistered => + 'We could not find an account associated with that email address.'; + + @override + String get errorDuplicateEmail => + 'This email address is already in use. Try another one or reset your password.'; + + @override + String get showDetailsAction => 'Show Details'; + + @override + String get errorLogin => 'Error logging in'; + + @override + String get errorCreatingInvitation => 'Failed to create invitaiton'; + + @override + String get footerCompanyName => 'Sibilla Solutions LTD'; + + @override + String get footerAddress => + '27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'; + + @override + String get footerSupport => 'Support'; + + @override + String get footerEmail => 'Email TBD'; + + @override + String get footerPhoneLabel => 'Phone'; + + @override + String get footerPhone => '+357 22 000 253'; + + @override + String get footerTermsOfService => 'Terms of Service'; + + @override + String get footerPrivacyPolicy => 'Privacy Policy'; + + @override + String get footerCookiePolicy => 'Cookie Policy'; + + @override + String get navigationLogout => 'Logout'; + + @override + String get dashboard => 'Dashboard'; + + @override + String get navigationUsersSettings => 'Users'; + + @override + String get navigationRolesSettings => 'Roles'; + + @override + String get navigationPermissionsSettings => 'Permissions'; + + @override + String get usersManagement => 'User Management'; + + @override + String get navigationOrganizationSettings => 'Organization settings'; + + @override + String get navigationAccountSettings => 'Profile settings'; + + @override + String get twoFactorPrompt => 'Enter the 6-digit code we sent to your device'; + + @override + String get twoFactorResend => 'Didn’t receive a code? Resend'; + + @override + String get twoFactorTitle => 'Two-Factor Authentication'; + + @override + String get twoFactorError => 'Invalid code. Please try again.'; + + @override + String get payoutNavDashboard => 'Dashboard'; + + @override + String get payoutNavSendPayout => 'Send payout'; + + @override + String get payoutNavRecipients => 'Recipients'; + + @override + String get payoutNavReports => 'Reports'; + + @override + String get payoutNavSettings => 'Settings'; + + @override + String get payoutNavLogout => 'Logout'; + + @override + String get payoutNavMethods => 'Payouts'; + + @override + String get expand => 'Expand'; + + @override + String get collapse => 'Collapse'; + + @override + String get pageTitleRecipients => 'Recipient address book'; + + @override + String get actionAddNew => 'Add new'; + + @override + String get colDataOwner => 'Data owner'; + + @override + String get colAvatar => 'Avatar'; + + @override + String get colName => 'Name'; + + @override + String get colEmail => 'Email'; + + @override + String get colStatus => 'Status'; + + @override + String get statusReady => 'Ready'; + + @override + String get statusRegistered => 'Registered'; + + @override + String get statusNotRegistered => 'Not registered'; + + @override + String get typeInternal => 'Managed by me'; + + @override + String get typeExternal => 'Self‑managed'; + + @override + String get searchHint => 'Search recipients'; + + @override + String get colActions => 'Actions'; + + @override + String get menuEdit => 'Edit'; + + @override + String get menuSendPayout => 'Send payout'; + + @override + String get tooltipRowActions => 'More actions'; + + @override + String get accountSettings => 'Account Settings'; + + @override + String get accountNameUpdateError => 'Failed to update account name'; + + @override + String get settingsSuccessfullyUpdated => 'Settings successfully updated'; + + @override + String get language => 'Language'; + + @override + String get failedToUpdateLanguage => 'Failed to update language'; + + @override + String get settingsImageUpdateError => 'Couldn\'t update the image'; + + @override + String get settingsImageTitle => 'Image'; + + @override + String get settingsImageHint => 'Tap to change the image'; + + @override + String get accountName => 'Name'; + + @override + String get accountNameHint => 'Specify your name'; + + @override + String get avatar => 'Profile photo'; + + @override + String get avatarHint => 'Tap to update'; + + @override + String get avatarUpdateError => 'Failed to update profile photo'; + + @override + String get settings => 'Settings'; + + @override + String get notSet => 'not set'; + + @override + String get search => 'Search...'; + + @override + String get ok => 'Ok'; + + @override + String get cancel => 'Cancel'; + + @override + String get confirm => 'Confirm'; + + @override + String get back => 'Back'; + + @override + String get operationfryTitle => 'Operation history'; + + @override + String get filters => 'Filters'; + + @override + String get period => 'Period'; + + @override + String get selectPeriod => 'Select period'; + + @override + String get apply => 'Apply'; + + @override + String status(String status) { + return '$status'; + } + + @override + String get operationStatusSuccessful => 'Successful'; + + @override + String get operationStatusPending => 'Pending'; + + @override + String get operationStatusUnsuccessful => 'Unsuccessful'; + + @override + String get statusColumn => 'Status'; + + @override + String get fileNameColumn => 'File name'; + + @override + String get amountColumn => 'Amount'; + + @override + String get toAmountColumn => 'To amount'; + + @override + String get payIdColumn => 'Pay ID'; + + @override + String get cardNumberColumn => 'Card number'; + + @override + String get nameColumn => 'Name'; + + @override + String get dateColumn => 'Date'; + + @override + String get commentColumn => 'Comment'; + + @override + String get paymentConfigTitle => 'Where to receive money'; + + @override + String get paymentConfigSubtitle => + 'Add multiple methods and choose your primary one.'; + + @override + String get addPaymentMethod => 'Add payment method'; + + @override + String get makeMain => 'Make primary'; + + @override + String get advanced => 'Advanced'; + + @override + String get fallbackExplanation => + 'If the primary method is unavailable, we will try the next enabled one in the list.'; + + @override + String get delete => 'Delete'; + + @override + String get deletePaymentConfirmation => + 'Are you sure you want to delete this payment method?'; + + @override + String get edit => 'Edit'; + + @override + String get moreActions => 'More actions'; + + @override + String get noPayouts => 'No Payouts'; + + @override + String get enterBankName => 'Enter bank name'; + + @override + String get paymentType => 'Payment Method Type'; + + @override + String get selectPaymentType => 'Please select a payment method type'; + + @override + String get paymentTypeCard => 'Credit Card'; + + @override + String get paymentTypeBankAccount => 'Russian Bank Account'; + + @override + String get paymentTypeIban => 'IBAN'; + + @override + String get paymentTypeWallet => 'Wallet'; + + @override + String get cardNumber => 'Card Number'; + + @override + String get enterCardNumber => 'Enter the card number'; + + @override + String get cardholderName => 'Cardholder Name'; + + @override + String get iban => 'IBAN'; + + @override + String get enterIban => 'Enter IBAN'; + + @override + String get bic => 'BIC'; + + @override + String get bankName => 'Bank Name'; + + @override + String get accountHolder => 'Account Holder'; + + @override + String get enterAccountHolder => 'Enter account holder'; + + @override + String get enterBic => 'Enter BIC'; + + @override + String get walletId => 'Wallet ID'; + + @override + String get enterWalletId => 'Enter wallet ID'; + + @override + String get recipients => 'Recipients'; + + @override + String get recipientName => 'Recipient Name'; + + @override + String get enterRecipientName => 'Enter recipient name'; + + @override + String get inn => 'INN'; + + @override + String get enterInn => 'Enter INN'; + + @override + String get kpp => 'KPP'; + + @override + String get enterKpp => 'Enter KPP'; + + @override + String get accountNumber => 'Account Number'; + + @override + String get enterAccountNumber => 'Enter account number'; + + @override + String get correspondentAccount => 'Correspondent Account'; + + @override + String get enterCorrespondentAccount => 'Enter correspondent account'; + + @override + String get bik => 'BIK'; + + @override + String get enterBik => 'Enter BIK'; + + @override + String get add => 'Add'; + + @override + String get expiryDate => 'Expiry (MM/YY)'; + + @override + String get firstName => 'First Name'; + + @override + String get enterFirstName => 'Enter First Name'; + + @override + String get lastName => 'Last Name'; + + @override + String get enterLastName => 'Enter Last Name'; + + @override + String get sendSingle => 'Send single transaction'; + + @override + String get sendMultiple => 'Send multiple transactions'; + + @override + String get addFunds => 'Add Funds'; + + @override + String get close => 'Close'; + + @override + String get multiplePayout => 'Multiple Payout'; + + @override + String get howItWorks => 'How it works?'; + + @override + String get exampleTitle => 'File Format & Sample'; + + @override + String get downloadSampleCSV => 'Download sample.csv'; + + @override + String get tokenColumn => 'Token (required)'; + + @override + String get currency => 'Currency'; + + @override + String get amount => 'Amount'; + + @override + String get comment => 'Comment'; + + @override + String get uploadCSV => 'Upload your CSV'; + + @override + String get upload => 'Upload'; + + @override + String get hintUpload => 'Supported format: .CSV · Max size 1 MB'; + + @override + String get uploadHistory => 'Upload History'; + + @override + String get payout => 'Payout'; + + @override + String get sendTo => 'Send Payout To'; + + @override + String get send => 'Send Payout'; + + @override + String get recipientPaysFee => 'Recipient pays the fee'; + + @override + String sentAmount(String amount) { + return 'Sent amount: \$$amount'; + } + + @override + String fee(String fee) { + return 'Fee: \$$fee'; + } + + @override + String recipientWillReceive(String amount) { + return 'Recipient will receive: \$$amount'; + } + + @override + String total(String total) { + return 'Total: \$$total'; + } + + @override + String get hideDetails => 'Hide Details'; + + @override + String get showDetails => 'Show Details'; + + @override + String get whereGetMoney => 'Source of funds for debit'; + + @override + String get details => 'Details'; + + @override + String get addRecipient => 'Add Recipient'; + + @override + String get editRecipient => 'Edit Recipient'; + + @override + String get saveRecipient => 'Save Recipient'; + + @override + String get choosePaymentMethod => 'Payment Methods (choose at least 1)'; + + @override + String get recipientFormRule => + 'Recipient must have at least one payment method'; + + @override + String get allStatus => 'All'; + + @override + String get readyStatus => 'Ready'; + + @override + String get registeredStatus => 'Registered'; + + @override + String get notRegisteredStatus => 'Not registered'; + + @override + String get noRecipientSelected => 'No recipient selected'; + + @override + String get companyName => 'Name of your company'; + + @override + String get companynameRequired => 'Company name required'; + + @override + String get errorSignUp => 'Error occured while signing up, try again later'; + + @override + String get companyDescription => 'Company Description'; + + @override + String get companyDescriptionHint => + 'Describe any of the fields of the Company\'s business'; + + @override + String get optional => 'optional'; +} diff --git a/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart b/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart new file mode 100644 index 0000000..caf30fe --- /dev/null +++ b/frontend/pweb/lib/generated/i18n/app_localizations_ru.dart @@ -0,0 +1,782 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Russian (`ru`). +class AppLocalizationsRu extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get login => 'Войти'; + + @override + String get logout => 'Выйти'; + + @override + String get profile => 'Профиль'; + + @override + String get signup => 'Регистрация'; + + @override + String get username => 'Email'; + + @override + String get usernameHint => 'email@example.com'; + + @override + String get usernameErrorInvalid => + 'Укажите действительный адрес электронной почты'; + + @override + String usernameUnknownTLD(Object domain) { + return 'Домен .$domain неизвестен, пожалуйста, проверьте его'; + } + + @override + String get password => 'Пароль'; + + @override + String get confirmPassword => 'Подтвердите пароль'; + + @override + String get passwordValidationRuleDigit => 'содержит цифру'; + + @override + String get passwordValidationRuleUpperCase => 'содержит заглавную букву'; + + @override + String get passwordValidationRuleLowerCase => 'содержит строчную букву'; + + @override + String get passwordValidationRuleSpecialCharacter => + 'содержит специальный символ'; + + @override + String passwordValidationRuleMinCharacters(Object charNum) { + return 'длина не менее $charNum символов'; + } + + @override + String get passwordsDoNotMatch => 'Пароли не совпадают'; + + @override + String passwordValidationError(Object matchesCriteria) { + return 'Убедитесь, что ваш пароль $matchesCriteria'; + } + + @override + String notificationError(Object error) { + return 'Произошла ошибка: $error'; + } + + @override + String loginUserNotFound(Object account) { + return 'Аккаунт $account не зарегистрирован в системе'; + } + + @override + String get loginPasswordIncorrect => + 'Ошибка авторизации, пожалуйста, проверьте пароль'; + + @override + String internalErrorOccurred(Object error) { + return 'Произошла внутренняя ошибка сервера: $error, мы уже знаем о ней и усердно работаем над исправлением'; + } + + @override + String get noErrorInformation => + 'Произошла ошибка, но у нас нет информации о ней. Мы уже расследуем этот вопрос'; + + @override + String get yourName => 'Ваше имя'; + + @override + String get nameHint => 'Иван Иванов'; + + @override + String get errorPageNotFoundTitle => 'Страница не найдена'; + + @override + String get errorPageNotFoundMessage => + 'Упс! Мы не смогли найти эту страницу.'; + + @override + String get errorPageNotFoundHint => + 'Запрашиваемая страница не существует или была перемещена. Пожалуйста, проверьте URL или вернитесь на главную страницу.'; + + @override + String get errorUnknown => 'Произошла неизвестная ошибка'; + + @override + String get unknown => 'неизвестно'; + + @override + String get goToLogin => 'Перейти к входу'; + + @override + String get goBack => 'Назад'; + + @override + String get goToMainPage => 'На главную'; + + @override + String get goToSignUp => 'Перейти к регистрации'; + + @override + String signupError(Object error) { + return 'Не удалось зарегистрироваться: $error'; + } + + @override + String signupSuccess(Object email) { + return 'Письмо с подтверждением email отправлено на $email. Пожалуйста, откройте его и перейдите по ссылке для активации вашего аккаунта.'; + } + + @override + String connectivityError(Object serverAddress) { + return 'Не удается связаться с сервером $serverAddress. Проверьте ваше интернет-соединение и попробуйте снова.'; + } + + @override + String get errorAccountExists => 'Account already exists'; + + @override + String get errorAccountNotVerified => + 'Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации'; + + @override + String get errorLoginUnauthorized => + 'Неверный логин или пароль. Пожалуйста, попробуйте снова'; + + @override + String get errorInternalError => + 'Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже'; + + @override + String get errorVerificationTokenNotFound => + 'Аккаунт для верификации не найден. Зарегистрируйтесь снова'; + + @override + String get created => 'Создано'; + + @override + String get edited => 'Изменено'; + + @override + String get errorDataConflict => + 'Мы не можем обработать ваши данные, так как они содержат конфликтующую или противоречивую информацию.'; + + @override + String get errorAccessDenied => + 'У вас нет разрешения на доступ к этому ресурсу. Если вам нужен доступ, пожалуйста, обратитесь к администратору.'; + + @override + String get errorBrokenPayload => + 'Отправленные данные недействительны или неполны. Пожалуйста, проверьте введенные данные и попробуйте снова.'; + + @override + String get errorInvalidArgument => + 'Один или несколько аргументов недействительны. Проверьте введенные данные и попробуйте снова.'; + + @override + String get errorBrokenReference => + 'Ресурс, к которому вы пытаетесь получить доступ, не может быть найден. Возможно, он был перемещен или удален.'; + + @override + String get errorInvalidQueryParameter => + 'Один или несколько параметров запроса отсутствуют или указаны неверно. Проверьте их и попробуйте снова.'; + + @override + String get errorNotImplemented => + 'Эта функция еще недоступна. Пожалуйста, попробуйте позже или обратитесь в службу поддержки.'; + + @override + String get errorLicenseRequired => + 'Для выполнения этого действия требуется действующая лицензия. Пожалуйста, обратитесь к вашему администратору.'; + + @override + String get errorNotFound => + 'Мы не смогли найти запрошенный ресурс. Возможно, он был удален или временно недоступен.'; + + @override + String get errorNameMissing => 'Пожалуйста, укажите имя для продолжения.'; + + @override + String get errorEmailMissing => + 'Пожалуйста, укажите адрес электронной почты для продолжения.'; + + @override + String get errorPasswordMissing => + 'Пожалуйста, укажите пароль для продолжения.'; + + @override + String get errorEmailNotRegistered => + 'Мы не нашли аккаунт, связанный с этим адресом электронной почты.'; + + @override + String get errorDuplicateEmail => + 'Этот адрес электронной почты уже используется. Попробуйте другой или восстановите пароль.'; + + @override + String get showDetailsAction => 'Показать детали'; + + @override + String get errorLogin => 'Ошибка входа'; + + @override + String get errorCreatingInvitation => 'Не удалось создать приглашение'; + + @override + String get footerCompanyName => 'Sibilla Solutions LTD'; + + @override + String get footerAddress => + '27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus'; + + @override + String get footerSupport => 'Поддержка'; + + @override + String get footerEmail => 'Email TBD'; + + @override + String get footerPhoneLabel => 'Телефон'; + + @override + String get footerPhone => '+357 22 000 253'; + + @override + String get footerTermsOfService => 'Условия обслуживания'; + + @override + String get footerPrivacyPolicy => 'Политика конфиденциальности'; + + @override + String get footerCookiePolicy => 'Политика использования файлов cookie'; + + @override + String get navigationLogout => 'Выйти'; + + @override + String get dashboard => 'Дашборд'; + + @override + String get navigationUsersSettings => 'Пользователи'; + + @override + String get navigationRolesSettings => 'Роли'; + + @override + String get navigationPermissionsSettings => 'Разрешения'; + + @override + String get usersManagement => 'Управление пользователями'; + + @override + String get navigationOrganizationSettings => 'Настройки организации'; + + @override + String get navigationAccountSettings => 'Настройки профиля'; + + @override + String get twoFactorPrompt => + 'Введите 6-значный код, отправленный на ваше устройство'; + + @override + String get twoFactorResend => 'Не получили код? Отправить снова'; + + @override + String get twoFactorTitle => 'Двухфакторная аутентификация'; + + @override + String get twoFactorError => 'Неверный код. Пожалуйста, попробуйте снова.'; + + @override + String get payoutNavDashboard => 'Дашборд'; + + @override + String get payoutNavSendPayout => 'Отправить выплату'; + + @override + String get payoutNavRecipients => 'Получатели'; + + @override + String get payoutNavReports => 'Отчеты'; + + @override + String get payoutNavSettings => 'Настройки'; + + @override + String get payoutNavLogout => 'Выйти'; + + @override + String get payoutNavMethods => 'Выплаты'; + + @override + String get expand => 'Развернуть'; + + @override + String get collapse => 'Свернуть'; + + @override + String get pageTitleRecipients => 'Адресная книга получателей'; + + @override + String get actionAddNew => 'Добавить'; + + @override + String get colDataOwner => 'Владелец данных'; + + @override + String get colAvatar => 'Аватар'; + + @override + String get colName => 'Имя'; + + @override + String get colEmail => 'Email'; + + @override + String get colStatus => 'Статус'; + + @override + String get statusReady => 'Готов'; + + @override + String get statusRegistered => 'Зарегистрирован'; + + @override + String get statusNotRegistered => 'Не зарегистрирован'; + + @override + String get typeInternal => 'Управляется мной'; + + @override + String get typeExternal => 'Самоуправляемый'; + + @override + String get searchHint => 'Поиск получателей'; + + @override + String get colActions => 'Действия'; + + @override + String get menuEdit => 'Редактировать'; + + @override + String get menuSendPayout => 'Отправить выплату'; + + @override + String get tooltipRowActions => 'Другие действия'; + + @override + String get accountSettings => 'Настройки аккаунта'; + + @override + String get accountNameUpdateError => 'Не удалось обновить имя аккаунта'; + + @override + String get settingsSuccessfullyUpdated => 'Настройки успешно обновлены'; + + @override + String get language => 'Язык'; + + @override + String get failedToUpdateLanguage => 'Не удалось обновить язык'; + + @override + String get settingsImageUpdateError => 'Не удалось обновить изображение'; + + @override + String get settingsImageTitle => 'Изображение'; + + @override + String get settingsImageHint => 'Нажмите, чтобы изменить изображение'; + + @override + String get accountName => 'Имя'; + + @override + String get accountNameHint => 'Укажите ваше имя'; + + @override + String get avatar => 'Фото профиля'; + + @override + String get avatarHint => 'Нажмите для обновления'; + + @override + String get avatarUpdateError => 'Не удалось обновить фото профиля'; + + @override + String get settings => 'Настройки'; + + @override + String get notSet => 'не задано'; + + @override + String get search => 'Поиск...'; + + @override + String get ok => 'Ок'; + + @override + String get cancel => 'Отмена'; + + @override + String get confirm => 'Подтвердить'; + + @override + String get back => 'Назад'; + + @override + String get operationfryTitle => 'История операций'; + + @override + String get filters => 'Фильтры'; + + @override + String get period => 'Период'; + + @override + String get selectPeriod => 'Выберите период'; + + @override + String get apply => 'Применить'; + + @override + String status(String status) { + return '$status'; + } + + @override + String get operationStatusSuccessful => 'Успешно'; + + @override + String get operationStatusPending => 'В ожидании'; + + @override + String get operationStatusUnsuccessful => 'Неуспешно'; + + @override + String get statusColumn => 'Статус'; + + @override + String get fileNameColumn => 'Имя файла'; + + @override + String get amountColumn => 'Сумма'; + + @override + String get toAmountColumn => 'На сумму'; + + @override + String get payIdColumn => 'Pay ID'; + + @override + String get cardNumberColumn => 'Номер карты'; + + @override + String get nameColumn => 'Имя'; + + @override + String get dateColumn => 'Дата'; + + @override + String get commentColumn => 'Комментарий'; + + @override + String get paymentConfigTitle => 'Куда получать деньги'; + + @override + String get paymentConfigSubtitle => + 'Добавьте несколько методов и выберите основной.'; + + @override + String get addPaymentMethod => 'Добавить способ оплаты'; + + @override + String get makeMain => 'Сделать основным'; + + @override + String get advanced => 'Дополнительно'; + + @override + String get fallbackExplanation => + 'Если основной метод недоступен, мы попробуем следующий включенный метод в списке.'; + + @override + String get delete => 'Удалить'; + + @override + String get deletePaymentConfirmation => + 'Вы уверены, что хотите удалить этот способ оплаты?'; + + @override + String get edit => 'Редактировать'; + + @override + String get moreActions => 'Еще действия'; + + @override + String get noPayouts => 'Нет выплат'; + + @override + String get enterBankName => 'Введите название банка'; + + @override + String get paymentType => 'Тип способа оплаты'; + + @override + String get selectPaymentType => 'Пожалуйста, выберите тип способа оплаты'; + + @override + String get paymentTypeCard => 'Кредитная карта'; + + @override + String get paymentTypeBankAccount => 'Российский банковский счет'; + + @override + String get paymentTypeIban => 'IBAN'; + + @override + String get paymentTypeWallet => 'Кошелек'; + + @override + String get cardNumber => 'Номер карты'; + + @override + String get enterCardNumber => 'Введите номер карты'; + + @override + String get cardholderName => 'Имя держателя карты'; + + @override + String get iban => 'IBAN'; + + @override + String get enterIban => 'Введите IBAN'; + + @override + String get bic => 'BIC'; + + @override + String get bankName => 'Название банка'; + + @override + String get accountHolder => 'Владелец счета'; + + @override + String get enterAccountHolder => 'Введите владельца счета'; + + @override + String get enterBic => 'Введите BIC'; + + @override + String get walletId => 'ID кошелька'; + + @override + String get enterWalletId => 'Введите ID кошелька'; + + @override + String get recipients => 'Получатели'; + + @override + String get recipientName => 'Имя получателя'; + + @override + String get enterRecipientName => 'Введите имя получателя'; + + @override + String get inn => 'ИНН'; + + @override + String get enterInn => 'Введите ИНН'; + + @override + String get kpp => 'КПП'; + + @override + String get enterKpp => 'Введите КПП'; + + @override + String get accountNumber => 'Номер счета'; + + @override + String get enterAccountNumber => 'Введите номер счета'; + + @override + String get correspondentAccount => 'Корреспондентский счет'; + + @override + String get enterCorrespondentAccount => 'Введите корреспондентский счет'; + + @override + String get bik => 'БИК'; + + @override + String get enterBik => 'Введите БИК'; + + @override + String get add => 'Добавить'; + + @override + String get expiryDate => 'Срок действия (ММ/ГГ)'; + + @override + String get firstName => 'Имя'; + + @override + String get enterFirstName => 'Введите имя'; + + @override + String get lastName => 'Фамилия'; + + @override + String get enterLastName => 'Введите фамилию'; + + @override + String get sendSingle => 'Отправить одну транзакцию'; + + @override + String get sendMultiple => 'Отправить несколько транзакций'; + + @override + String get addFunds => 'Пополнить счет'; + + @override + String get close => 'Закрыть'; + + @override + String get multiplePayout => 'Множественная выплата'; + + @override + String get howItWorks => 'Как это работает?'; + + @override + String get exampleTitle => 'Формат файла и образец'; + + @override + String get downloadSampleCSV => 'Скачать sample.csv'; + + @override + String get tokenColumn => 'Токен (обязательно)'; + + @override + String get currency => 'Валюта'; + + @override + String get amount => 'Сумма'; + + @override + String get comment => 'Комментарий'; + + @override + String get uploadCSV => 'Загрузите ваш CSV'; + + @override + String get upload => 'Загрузить'; + + @override + String get hintUpload => 'Поддерживаемый формат: .CSV · Макс. размер 1 МБ'; + + @override + String get uploadHistory => 'История загрузок'; + + @override + String get payout => 'Выплата'; + + @override + String get sendTo => 'Отправить выплату'; + + @override + String get send => 'Отправить выплату'; + + @override + String get recipientPaysFee => 'Получатель оплачивает комиссию'; + + @override + String sentAmount(String amount) { + return 'Отправленная сумма: \$$amount'; + } + + @override + String fee(String fee) { + return 'Комиссия: \$$fee'; + } + + @override + String recipientWillReceive(String amount) { + return 'Получатель получит: \$$amount'; + } + + @override + String total(String total) { + return 'Итого: \$$total'; + } + + @override + String get hideDetails => 'Скрыть детали'; + + @override + String get showDetails => 'Показать детали'; + + @override + String get whereGetMoney => 'Источник средств для списания'; + + @override + String get details => 'Детали'; + + @override + String get addRecipient => 'Добавить получателя'; + + @override + String get editRecipient => 'Редактировать получателя'; + + @override + String get saveRecipient => 'Сохранить получателя'; + + @override + String get choosePaymentMethod => 'Способы оплаты (выберите хотя бы 1)'; + + @override + String get recipientFormRule => + 'Получатель должен иметь хотя бы один способ оплаты'; + + @override + String get allStatus => 'Все'; + + @override + String get readyStatus => 'Готов'; + + @override + String get registeredStatus => 'Зарегистрирован'; + + @override + String get notRegisteredStatus => 'Не зарегистрирован'; + + @override + String get noRecipientSelected => 'Получатель не выбран'; + + @override + String get companyName => 'Name of your company'; + + @override + String get companynameRequired => 'Company name required'; + + @override + String get errorSignUp => 'Error occured while signing up, try again later'; + + @override + String get companyDescription => 'Company Description'; + + @override + String get companyDescriptionHint => + 'Describe any of the fields of the Company\'s business'; + + @override + String get optional => 'optional'; +} diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb new file mode 100644 index 0000000..b152d02 --- /dev/null +++ b/frontend/pweb/lib/l10n/en.arb @@ -0,0 +1,435 @@ +{ + "@@locale": "en", + "login": "Login", + "logout": "Logout", + "profile": "Profile", + "signup": "Sign up", + "username": "Email", + "usernameHint": "email@example.com", + "usernameErrorInvalid": "Provide a valid email address", + "usernameUnknownTLD": "Domain .{domain} is not known, please, check it", + "password": "Password", + "confirmPassword": "Confirm password", + "passwordValidationRuleDigit": "has digit", + "passwordValidationRuleUpperCase": "has uppercase letter", + "passwordValidationRuleLowerCase": "has lowercase letter", + "passwordValidationRuleSpecialCharacter": "has special character letter", + "passwordValidationRuleMinCharacters": "is {charNum} characters long at least", + "passwordsDoNotMatch": "Passwords do not match", + "passwordValidationError": "Check that your password {matchesCriteria}", + "notificationError": "Error occurred: {error}", + "loginUserNotFound": "Account {account} has not been registered in the system", + "loginPasswordIncorrect": "Authorization failed, please check your password", + "internalErrorOccurred": "An internal server error occurred: {error}, we already know about it and working hard to fix it", + "noErrorInformation": "Some error occurred, but we have not error information. We are already investigating the issue", + "yourName": "Your name", + "nameHint": "John Doe", + "errorPageNotFoundTitle": "Page Not Found", + "errorPageNotFoundMessage": "Oops! We couldn't find that page.", + "errorPageNotFoundHint": "The page you're looking for doesn't exist or has been moved. Please check the URL or return to the home page.", + "errorUnknown": "Unknown error occurred", + "unknown": "unknown", + "goToLogin": "Go to Login", + "goBack": "Go Back", + "goToMainPage": "Go to Main Page", + "goToSignUp": "Go to Sign Up", + "signupError": "Failed to signup: {error}", + "signupSuccess": "Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.", + "connectivityError": "Cannot reach the server at {serverAddress}. Check your network and try again.", + "errorAccountExists": "Account already exists", + "errorAccountNotVerified": "Your account hasn't been verified yet. Please check your email to complete the verification", + "errorLoginUnauthorized": "Login or password is incorrect. Please try again", + "errorInternalError": "An internal error occurred. We're aware of the issue and working to resolve it. Please try again later", + "errorVerificationTokenNotFound": "Account for verification not found. Sign up again", + "created": "Created", + "edited": "Edited", + "errorDataConflict": "We can’t process your data because it has conflicting or contradictory information.", + "errorAccessDenied": "You do not have permission to access this resource. If you need access, please contact an administrator.", + "errorBrokenPayload": "The data you sent is invalid or incomplete. Please check your submission and try again.", + "errorInvalidArgument": "One or more arguments are invalid. Verify your input and try again.", + "errorBrokenReference": "The resource you're trying to access could not be referenced. It may have been moved or deleted.", + "errorInvalidQueryParameter": "One or more query parameters are missing or incorrect. Check them and try again.", + "errorNotImplemented": "This feature is not yet available. Please try again later or contact support.", + "errorLicenseRequired": "A valid license is required to perform this action. Please contact your administrator.", + "errorNotFound": "We couldn't find the resource you requested. It may have been removed or is temporarily unavailable.", + "errorNameMissing": "Please provide a name before continuing.", + "errorEmailMissing": "Please provide an email address before continuing.", + "errorPasswordMissing": "Please provide a password before continuing.", + "errorEmailNotRegistered": "We could not find an account associated with that email address.", + "errorDuplicateEmail": "This email address is already in use. Try another one or reset your password.", + "showDetailsAction": "Show Details", + "errorLogin": "Error logging in", + "errorCreatingInvitation": "Failed to create invitaiton", + "@errorCreatingInvitation": { + "description": "Error message displayed when invitation creation fails" + }, + "footerCompanyName": "Sibilla Solutions LTD", + "footerAddress": "27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus", + "footerSupport": "Support", + "footerEmail": "Email TBD", + "footerPhoneLabel": "Phone", + "footerPhone": "+357 22 000 253", + "footerTermsOfService": "Terms of Service", + "footerPrivacyPolicy": "Privacy Policy", + "footerCookiePolicy": "Cookie Policy", + "navigationLogout": "Logout", + "dashboard": "Dashboard", + "navigationUsersSettings": "Users", + "navigationRolesSettings": "Roles", + "navigationPermissionsSettings": "Permissions", + "usersManagement": "User Management", + "navigationOrganizationSettings": "Organization settings", + "navigationAccountSettings": "Profile settings", + "twoFactorPrompt": "Enter the 6-digit code we sent to your device", + "twoFactorResend": "Didn’t receive a code? Resend", + "twoFactorTitle": "Two-Factor Authentication", + "twoFactorError": "Invalid code. Please try again.", + "payoutNavDashboard": "Dashboard", + "payoutNavSendPayout": "Send payout", + "payoutNavRecipients": "Recipients", + "payoutNavReports": "Reports", + "payoutNavSettings": "Settings", + "payoutNavLogout": "Logout", + "payoutNavMethods": "Payouts", + "expand": "Expand", + "collapse": "Collapse", + "pageTitleRecipients": "Recipient address book", + "@pageTitleRecipients": { + "description": "Title of the recipient address book page", + "type": "text" + }, + + "actionAddNew": "Add new", + "@actionAddNew": { + "description": "Tooltip and button label to add a new recipient" + }, + + "colDataOwner": "Data owner", + "@colDataOwner": { + "description": "Column header for who manages the payout data" + }, + + "colAvatar": "Avatar", + "@colAvatar": { + "description": "Column header for recipient avatar" + }, + + "colName": "Name", + "@colName": { + "description": "Column header for recipient name" + }, + + "colEmail": "Email", + "@colEmail": { + "description": "Column header for recipient email address" + }, + + "colStatus": "Status", + "@colStatus": { + "description": "Column header for payout readiness status" + }, + + "statusReady": "Ready", + "@statusReady": { + "description": "Status indicating payouts can be sent immediately" + }, + + "statusRegistered": "Registered", + "@statusRegistered": { + "description": "Status indicating recipient is registered but not yet fully ready" + }, + + "statusNotRegistered": "Not registered", + "@statusNotRegistered": { + "description": "Status indicating recipient has not completed registration" + }, + + "typeInternal": "Managed by me", + "@typeInternal": { + "description": "Label for recipients whose payout data is managed internally by the user/company" + }, + + "typeExternal": "Self‑managed", + "@typeExternal": { + "description": "Label for recipients who manage their own payout data" + }, + + "searchHint": "Search recipients", + "colActions": "Actions", + "menuEdit": "Edit", + "menuSendPayout": "Send payout", + "tooltipRowActions": "More actions", + "accountSettings": "Account Settings", + "accountNameUpdateError": "Failed to update account name", + "settingsSuccessfullyUpdated": "Settings successfully updated", + "language": "Language", + "failedToUpdateLanguage": "Failed to update language", + "settingsImageUpdateError": "Couldn't update the image", + "settingsImageTitle": "Image", + "settingsImageHint": "Tap to change the image", + "accountName": "Name", + "accountNameHint": "Specify your name", + "avatar": "Profile photo", + "avatarHint": "Tap to update", + "avatarUpdateError": "Failed to update profile photo", + "settings": "Settings", + "notSet": "not set", + "search": "Search...", + "ok": "Ok", + "cancel": "Cancel", + "confirm": "Confirm", + "back": "Back", + + "operationfryTitle": "Operation history", + "@operationfryTitle": { + "description": "Title of the operation history page" + }, + + "filters": "Filters", + "@filters": { + "description": "Label for the filters expansion panel" + }, + + "period": "Period", + "@period": { + "description": "Label for the date‐range filter" + }, + + "selectPeriod": "Select period", + "@selectPeriod": { + "description": "Placeholder when no period is selected" + }, + + "apply": "Apply", + "@apply": { + "description": "Button text to apply the filters" + }, + + "status": "{status}", + "@status": { + "description": "Template for a single status filter chip", + "placeholders": { + "status": { + "type": "String", + "example": "Successful" + } + } + }, + + "operationStatusSuccessful": "Successful", + "@operationStatusSuccessful": { + "description": "Status indicating the operation succeeded" + }, + + "operationStatusPending": "Pending", + "@operationStatusPending": { + "description": "Status indicating the operation is pending" + }, + + "operationStatusUnsuccessful": "Unsuccessful", + "@operationStatusUnsuccessful": { + "description": "Status indicating the operation failed" + }, + + "statusColumn": "Status", + "@statusColumn": { + "description": "Table column header for status" + }, + + "fileNameColumn": "File name", + "@fileNameColumn": { + "description": "Table column header for file name" + }, + + "amountColumn": "Amount", + "@amountColumn": { + "description": "Table column header for the original amount" + }, + + "toAmountColumn": "To amount", + "@toAmountColumn": { + "description": "Table column header for the converted amount" + }, + + "payIdColumn": "Pay ID", + "@payIdColumn": { + "description": "Table column header for the payment ID" + }, + + "cardNumberColumn": "Card number", + "@cardNumberColumn": { + "description": "Table column header for the masked card number" + }, + + "nameColumn": "Name", + "@nameColumn": { + "description": "Table column header for recipient name" + }, + + "dateColumn": "Date", + "@dateColumn": { + "description": "Table column header for the date/time" + }, + + "commentColumn": "Comment", + "@commentColumn": { + "description": "Table column header for any comment" + }, + "paymentConfigTitle": "Where to receive money", + "paymentConfigSubtitle": "Add multiple methods and choose your primary one.", + "addPaymentMethod": "Add payment method", + "makeMain": "Make primary", + "advanced": "Advanced", + "fallbackExplanation": "If the primary method is unavailable, we will try the next enabled one in the list.", + "delete": "Delete", + "@delete": { + "description": "Button label to delete a payment method" + }, + + "deletePaymentConfirmation": "Are you sure you want to delete this payment method?", + "@deletePaymentConfirmation": { + "description": "Confirmation dialog message shown before a payment method is removed" + }, + + "edit": "Edit", + "@edit": { + "description": "Button label to edit a payment method" + }, + + "moreActions": "More actions", + "@moreActions": { + "description": "Tooltip for an overflow menu button that reveals extra actions for a payment method" + }, + "noPayouts": "No Payouts", + + "enterBankName": "Enter bank name", + + "paymentType": "Payment Method Type", + "selectPaymentType": "Please select a payment method type", + + "paymentTypeCard": "Credit Card", + "paymentTypeBankAccount": "Russian Bank Account", + "paymentTypeIban": "IBAN", + "paymentTypeWallet": "Wallet", + + "cardNumber": "Card Number", + "enterCardNumber": "Enter the card number", + "cardholderName": "Cardholder Name", + + "iban": "IBAN", + "enterIban": "Enter IBAN", + "bic": "BIC", + "bankName": "Bank Name", + "accountHolder": "Account Holder", + "enterAccountHolder": "Enter account holder", + "enterBic": "Enter BIC", + + "walletId": "Wallet ID", + "enterWalletId": "Enter wallet ID", + + "recipients": "Recipients", + "recipientName": "Recipient Name", + "enterRecipientName": "Enter recipient name", + "inn": "INN", + "enterInn": "Enter INN", + "kpp": "KPP", + "enterKpp": "Enter KPP", + "accountNumber": "Account Number", + "enterAccountNumber": "Enter account number", + "correspondentAccount": "Correspondent Account", + "enterCorrespondentAccount": "Enter correspondent account", + "bik": "BIK", + "enterBik": "Enter BIK", + "add": "Add", + "expiryDate": "Expiry (MM/YY)", + "firstName": "First Name", + "enterFirstName": "Enter First Name", + "lastName": "Last Name", + "enterLastName": "Enter Last Name", + "sendSingle": "Send single transaction", + "sendMultiple": "Send multiple transactions", + "addFunds": "Add Funds", + "close": "Close", + "multiplePayout": "Multiple Payout", + "howItWorks": "How it works?", + "exampleTitle": "File Format & Sample", + "downloadSampleCSV": "Download sample.csv", + "tokenColumn": "Token (required)", + "currency": "Currency", + "amount": "Amount", + "comment": "Comment", + "uploadCSV": "Upload your CSV", + "upload": "Upload", + "hintUpload": "Supported format: .CSV · Max size 1 MB", + "uploadHistory": "Upload History", + "payout": "Payout", + "sendTo": "Send Payout To", + "send": "Send Payout", + "recipientPaysFee": "Recipient pays the fee", + + "sentAmount": "Sent amount: ${amount}", + "@sentAmount": { + "description": "Label showing the amount sent", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + + "fee": "Fee: ${fee}", + "@fee": { + "description": "Label showing the transaction fee", + "placeholders": { + "fee": { + "type": "String" + } + } + }, + + "recipientWillReceive": "Recipient will receive: ${amount}", + "@recipientWillReceive": { + "description": "Label showing how much the recipient will receive", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + + "total": "Total: ${total}", + "@total": { + "description": "Label showing the total amount of the transaction", + "placeholders": { + "total": { + "type": "String" + } + } + }, + "hideDetails": "Hide Details", + "showDetails": "Show Details", + "whereGetMoney": "Source of funds for debit", + "details": "Details", + + "addRecipient": "Add Recipient", + "editRecipient": "Edit Recipient", + "saveRecipient": "Save Recipient", + + "choosePaymentMethod": "Payment Methods (choose at least 1)", + "recipientFormRule": "Recipient must have at least one payment method", + + "allStatus": "All", + "readyStatus": "Ready", + "registeredStatus": "Registered", + "notRegisteredStatus": "Not registered", + + "noRecipientSelected": "No recipient selected", + + "companyName": "Name of your company", + "companynameRequired": "Company name required", + + "errorSignUp": "Error occured while signing up, try again later", + "companyDescription": "Company Description", + "companyDescriptionHint": "Describe any of the fields of the Company's business", + "optional": "optional" +} \ No newline at end of file diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb new file mode 100644 index 0000000..def1290 --- /dev/null +++ b/frontend/pweb/lib/l10n/ru.arb @@ -0,0 +1,426 @@ +{ + "@@locale": "ru", + "login": "Войти", + "logout": "Выйти", + "profile": "Профиль", + "signup": "Регистрация", + "username": "Email", + "usernameHint": "email@example.com", + "usernameErrorInvalid": "Укажите действительный адрес электронной почты", + "usernameUnknownTLD": "Домен .{domain} неизвестен, пожалуйста, проверьте его", + "password": "Пароль", + "confirmPassword": "Подтвердите пароль", + "passwordValidationRuleDigit": "содержит цифру", + "passwordValidationRuleUpperCase": "содержит заглавную букву", + "passwordValidationRuleLowerCase": "содержит строчную букву", + "passwordValidationRuleSpecialCharacter": "содержит специальный символ", + "passwordValidationRuleMinCharacters": "длина не менее {charNum} символов", + "passwordsDoNotMatch": "Пароли не совпадают", + "passwordValidationError": "Убедитесь, что ваш пароль {matchesCriteria}", + "notificationError": "Произошла ошибка: {error}", + "loginUserNotFound": "Аккаунт {account} не зарегистрирован в системе", + "loginPasswordIncorrect": "Ошибка авторизации, пожалуйста, проверьте пароль", + "internalErrorOccurred": "Произошла внутренняя ошибка сервера: {error}, мы уже знаем о ней и усердно работаем над исправлением", + "noErrorInformation": "Произошла ошибка, но у нас нет информации о ней. Мы уже расследуем этот вопрос", + "yourName": "Ваше имя", + "nameHint": "Иван Иванов", + "errorPageNotFoundTitle": "Страница не найдена", + "errorPageNotFoundMessage": "Упс! Мы не смогли найти эту страницу.", + "errorPageNotFoundHint": "Запрашиваемая страница не существует или была перемещена. Пожалуйста, проверьте URL или вернитесь на главную страницу.", + "errorUnknown": "Произошла неизвестная ошибка", + "unknown": "неизвестно", + "goToLogin": "Перейти к входу", + "goBack": "Назад", + "goToMainPage": "На главную", + "goToSignUp": "Перейти к регистрации", + "signupError": "Не удалось зарегистрироваться: {error}", + "signupSuccess": "Письмо с подтверждением email отправлено на {email}. Пожалуйста, откройте его и перейдите по ссылке для активации вашего аккаунта.", + "connectivityError": "Не удается связаться с сервером {serverAddress}. Проверьте ваше интернет-соединение и попробуйте снова.", + "errorAccountNotVerified": "Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации", + "errorLoginUnauthorized": "Неверный логин или пароль. Пожалуйста, попробуйте снова", + "errorInternalError": "Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже", + "errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова", + "created": "Создано", + "edited": "Изменено", + "errorDataConflict": "Мы не можем обработать ваши данные, так как они содержат конфликтующую или противоречивую информацию.", + "errorAccessDenied": "У вас нет разрешения на доступ к этому ресурсу. Если вам нужен доступ, пожалуйста, обратитесь к администратору.", + "errorBrokenPayload": "Отправленные данные недействительны или неполны. Пожалуйста, проверьте введенные данные и попробуйте снова.", + "errorInvalidArgument": "Один или несколько аргументов недействительны. Проверьте введенные данные и попробуйте снова.", + "errorBrokenReference": "Ресурс, к которому вы пытаетесь получить доступ, не может быть найден. Возможно, он был перемещен или удален.", + "errorInvalidQueryParameter": "Один или несколько параметров запроса отсутствуют или указаны неверно. Проверьте их и попробуйте снова.", + "errorNotImplemented": "Эта функция еще недоступна. Пожалуйста, попробуйте позже или обратитесь в службу поддержки.", + "errorLicenseRequired": "Для выполнения этого действия требуется действующая лицензия. Пожалуйста, обратитесь к вашему администратору.", + "errorNotFound": "Мы не смогли найти запрошенный ресурс. Возможно, он был удален или временно недоступен.", + "errorNameMissing": "Пожалуйста, укажите имя для продолжения.", + "errorEmailMissing": "Пожалуйста, укажите адрес электронной почты для продолжения.", + "errorPasswordMissing": "Пожалуйста, укажите пароль для продолжения.", + "errorEmailNotRegistered": "Мы не нашли аккаунт, связанный с этим адресом электронной почты.", + "errorDuplicateEmail": "Этот адрес электронной почты уже используется. Попробуйте другой или восстановите пароль.", + "showDetailsAction": "Показать детали", + "errorLogin": "Ошибка входа", + "errorCreatingInvitation": "Не удалось создать приглашение", + "@errorCreatingInvitation": { + "description": "Сообщение об ошибке, отображаемое при неудачном создании приглашения" + }, + "footerCompanyName": "Sibilla Solutions LTD", + "footerAddress": "27, Pindarou Street, Alpha Business Centre, Block B 7th Floor, 1060 Nicosia, Cyprus", + "footerSupport": "Поддержка", + "footerEmail": "Email TBD", + "footerPhoneLabel": "Телефон", + "footerPhone": "+357 22 000 253", + "footerTermsOfService": "Условия обслуживания", + "footerPrivacyPolicy": "Политика конфиденциальности", + "footerCookiePolicy": "Политика использования файлов cookie", + "navigationLogout": "Выйти", + "dashboard": "Дашборд", + "navigationUsersSettings": "Пользователи", + "navigationRolesSettings": "Роли", + "navigationPermissionsSettings": "Разрешения", + "usersManagement": "Управление пользователями", + "navigationOrganizationSettings": "Настройки организации", + "navigationAccountSettings": "Настройки профиля", + "twoFactorPrompt": "Введите 6-значный код, отправленный на ваше устройство", + "twoFactorResend": "Не получили код? Отправить снова", + "twoFactorTitle": "Двухфакторная аутентификация", + "twoFactorError": "Неверный код. Пожалуйста, попробуйте снова.", + "payoutNavDashboard": "Дашборд", + "payoutNavSendPayout": "Отправить выплату", + "payoutNavRecipients": "Получатели", + "payoutNavReports": "Отчеты", + "payoutNavSettings": "Настройки", + "payoutNavLogout": "Выйти", + "payoutNavMethods": "Выплаты", + "expand": "Развернуть", + "collapse": "Свернуть", + "pageTitleRecipients": "Адресная книга получателей", + "@pageTitleRecipients": { + "description": "Заголовок страницы адресной книги получателей", + "type": "text" + }, + + "actionAddNew": "Добавить", + "@actionAddNew": { + "description": "Подсказка и метка кнопки для добавления нового получателя" + }, + + "colDataOwner": "Владелец данных", + "@colDataOwner": { + "description": "Заголовок столбца для указания, кто управляет данными о выплатах" + }, + + "colAvatar": "Аватар", + "@colAvatar": { + "description": "Заголовок столбца для аватара получателя" + }, + + "colName": "Имя", + "@colName": { + "description": "Заголовок столбца для имени получателя" + }, + + "colEmail": "Email", + "@colEmail": { + "description": "Заголовок столбца для адреса электронной почты получателя" + }, + + "colStatus": "Статус", + "@colStatus": { + "description": "Заголовок столбца для статуса готовности к выплате" + }, + + "statusReady": "Готов", + "@statusReady": { + "description": "Статус, указывающий, что выплаты можно отправлять немедленно" + }, + + "statusRegistered": "Зарегистрирован", + "@statusRegistered": { + "description": "Статус, указывающий, что получатель зарегистрирован, но еще не полностью готов" + }, + + "statusNotRegistered": "Не зарегистрирован", + "@statusNotRegistered": { + "description": "Статус, указывающий, что получатель не завершил регистрацию" + }, + + "typeInternal": "Управляется мной", + "@typeInternal": { + "description": "Метка для получателей, чьи данные о выплатах управляются внутренне пользователем/компанией" + }, + + "typeExternal": "Самоуправляемый", + "@typeExternal": { + "description": "Метка для получателей, которые управляют своими данными о выплатах самостоятельно" + }, + + "searchHint": "Поиск получателей", + "colActions": "Действия", + "menuEdit": "Редактировать", + "menuSendPayout": "Отправить выплату", + "tooltipRowActions": "Другие действия", + "accountSettings": "Настройки аккаунта", + "accountNameUpdateError": "Не удалось обновить имя аккаунта", + "settingsSuccessfullyUpdated": "Настройки успешно обновлены", + "language": "Язык", + "failedToUpdateLanguage": "Не удалось обновить язык", + "settingsImageUpdateError": "Не удалось обновить изображение", + "settingsImageTitle": "Изображение", + "settingsImageHint": "Нажмите, чтобы изменить изображение", + "accountName": "Имя", + "accountNameHint": "Укажите ваше имя", + "avatar": "Фото профиля", + "avatarHint": "Нажмите для обновления", + "avatarUpdateError": "Не удалось обновить фото профиля", + "settings": "Настройки", + "notSet": "не задано", + "search": "Поиск...", + "ok": "Ок", + "cancel": "Отмена", + "confirm": "Подтвердить", + "back": "Назад", + + "operationfryTitle": "История операций", + "@operationfryTitle": { + "description": "Заголовок страницы истории операций" + }, + + "filters": "Фильтры", + "@filters": { + "description": "Метка для панели расширения фильтров" + }, + + "period": "Период", + "@period": { + "description": "Метка для фильтра по диапазону дат" + }, + + "selectPeriod": "Выберите период", + "@selectPeriod": { + "description": "Заполнитель, когда период не выбран" + }, + + "apply": "Применить", + "@apply": { + "description": "Текст кнопки для применения фильтров" + }, + + "status": "{status}", + "@status": { + "description": "Шаблон для одного чипа фильтра статуса", + "placeholders": { + "status": { + "type": "String", + "example": "Успешно" + } + } + }, + + "operationStatusSuccessful": "Успешно", + "@operationStatusSuccessful": { + "description": "Статус, указывающий на успешное выполнение операции" + }, + + "operationStatusPending": "В ожидании", + "@operationStatusPending": { + "description": "Статус, указывающий, что операция ожидает выполнения" + }, + + "operationStatusUnsuccessful": "Неуспешно", + "@operationStatusUnsuccessful": { + "description": "Статус, указывающий на сбой операции" + }, + + "statusColumn": "Статус", + "@statusColumn": { + "description": "Заголовок столбца таблицы для статуса" + }, + + "fileNameColumn": "Имя файла", + "@fileNameColumn": { + "description": "Заголовок столбца таблицы для имени файла" + }, + + "amountColumn": "Сумма", + "@amountColumn": { + "description": "Заголовок столбца таблицы для исходной суммы" + }, + + "toAmountColumn": "На сумму", + "@toAmountColumn": { + "description": "Заголовок столбца таблицы для конвертированной суммы" + }, + + "payIdColumn": "Pay ID", + "@payIdColumn": { + "description": "Заголовок столбца таблицы для идентификатора платежа" + }, + + "cardNumberColumn": "Номер карты", + "@cardNumberColumn": { + "description": "Заголовок столбца таблицы для замаскированного номера карты" + }, + + "nameColumn": "Имя", + "@nameColumn": { + "description": "Заголовок столбца таблицы для имени получателя" + }, + + "dateColumn": "Дата", + "@dateColumn": { + "description": "Заголовок столбца таблицы для даты/времени" + }, + + "commentColumn": "Комментарий", + "@commentColumn": { + "description": "Заголовок столбца таблицы для комментария" + }, + "paymentConfigTitle": "Куда получать деньги", + "paymentConfigSubtitle": "Добавьте несколько методов и выберите основной.", + "addPaymentMethod": "Добавить способ оплаты", + "makeMain": "Сделать основным", + "advanced": "Дополнительно", + "fallbackExplanation": "Если основной метод недоступен, мы попробуем следующий включенный метод в списке.", + "delete": "Удалить", + "@delete": { + "description": "Метка кнопки для удаления способа оплаты" + }, + + "deletePaymentConfirmation": "Вы уверены, что хотите удалить этот способ оплаты?", + "@deletePaymentConfirmation": { + "description": "Сообщение диалога подтверждения, показываемое перед удалением способа оплаты" + }, + + "edit": "Редактировать", + "@edit": { + "description": "Метка кнопки для редактирования способа оплаты" + }, + + "moreActions": "Еще действия", + "@moreActions": { + "description": "Подсказка для кнопки меню с многоточием, открывающей дополнительные действия для способа оплаты" + }, + "noPayouts": "Нет выплат", + + "enterBankName": "Введите название банка", + + "paymentType": "Тип способа оплаты", + "selectPaymentType": "Пожалуйста, выберите тип способа оплаты", + + "paymentTypeCard": "Кредитная карта", + "paymentTypeBankAccount": "Российский банковский счет", + "paymentTypeIban": "IBAN", + "paymentTypeWallet": "Кошелек", + + "cardNumber": "Номер карты", + "enterCardNumber": "Введите номер карты", + "cardholderName": "Имя держателя карты", + + "iban": "IBAN", + "enterIban": "Введите IBAN", + "bic": "BIC", + "bankName": "Название банка", + "accountHolder": "Владелец счета", + "enterAccountHolder": "Введите владельца счета", + "enterBic": "Введите BIC", + + "walletId": "ID кошелька", + "enterWalletId": "Введите ID кошелька", + + "recipients": "Получатели", + "recipientName": "Имя получателя", + "enterRecipientName": "Введите имя получателя", + "inn": "ИНН", + "enterInn": "Введите ИНН", + "kpp": "КПП", + "enterKpp": "Введите КПП", + "accountNumber": "Номер счета", + "enterAccountNumber": "Введите номер счета", + "correspondentAccount": "Корреспондентский счет", + "enterCorrespondentAccount": "Введите корреспондентский счет", + "bik": "БИК", + "enterBik": "Введите БИК", + "add": "Добавить", + "expiryDate": "Срок действия (ММ/ГГ)", + "firstName": "Имя", + "enterFirstName": "Введите имя", + "lastName": "Фамилия", + "enterLastName": "Введите фамилию", + "sendSingle": "Отправить одну транзакцию", + "sendMultiple": "Отправить несколько транзакций", + "addFunds": "Пополнить счет", + "close": "Закрыть", + "multiplePayout": "Множественная выплата", + "howItWorks": "Как это работает?", + "exampleTitle": "Формат файла и образец", + "downloadSampleCSV": "Скачать sample.csv", + "tokenColumn": "Токен (обязательно)", + "currency": "Валюта", + "amount": "Сумма", + "comment": "Комментарий", + "uploadCSV": "Загрузите ваш CSV", + "upload": "Загрузить", + "hintUpload": "Поддерживаемый формат: .CSV · Макс. размер 1 МБ", + "uploadHistory": "История загрузок", + "payout": "Выплата", + "sendTo": "Отправить выплату", + "send": "Отправить выплату", + "recipientPaysFee": "Получатель оплачивает комиссию", + + "sentAmount": "Отправленная сумма: ${amount}", + "@sentAmount": { + "description": "Метка, показывающая отправленную сумму", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + + "fee": "Комиссия: ${fee}", + "@fee": { + "description": "Метка, показывающая комиссию за транзакцию", + "placeholders": { + "fee": { + "type": "String" + } + } + }, + + "recipientWillReceive": "Получатель получит: ${amount}", + "@recipientWillReceive": { + "description": "Метка, показывающая, сколько получит получатель", + "placeholders": { + "amount": { + "type": "String" + } + } + }, + + "total": "Итого: ${total}", + "@total": { + "description": "Метка, показывающая общую сумму транзакции", + "placeholders": { + "total": { + "type": "String" + } + } + }, + "hideDetails": "Скрыть детали", + "showDetails": "Показать детали", + "whereGetMoney": "Источник средств для списания", + "details": "Детали", + + "addRecipient": "Добавить получателя", + "editRecipient": "Редактировать получателя", + "saveRecipient": "Сохранить получателя", + + "choosePaymentMethod": "Способы оплаты (выберите хотя бы 1)", + "recipientFormRule": "Получатель должен иметь хотя бы один способ оплаты", + + "allStatus": "Все", + "readyStatus": "Готов", + "registeredStatus": "Зарегистрирован", + "notRegisteredStatus": "Не зарегистрирован", + + "noRecipientSelected": "Получатель не выбран" +} \ No newline at end of file diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart new file mode 100644 index 0000000..0ee7f60 --- /dev/null +++ b/frontend/pweb/lib/main.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +// ignore: depend_on_referenced_packages +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'package:provider/provider.dart'; + +import 'package:logging/logging.dart'; + +import 'package:pshared/config/constants.dart'; +import 'package:pshared/provider/account.dart'; +import 'package:pshared/provider/locale.dart'; +import 'package:pshared/provider/organizations.dart'; +import 'package:pshared/provider/pfe/provider.dart'; + +import 'package:pweb/app/app.dart'; +import 'package:pweb/app/timeago.dart'; +import 'package:pweb/providers/balance.dart'; +import 'package:pweb/providers/carousel.dart'; +import 'package:pweb/providers/mock_payment.dart'; +import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/providers/payment_methods.dart'; +import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/providers/two_factor.dart'; +import 'package:pweb/providers/upload_history.dart'; +import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/services/amplitude.dart'; +import 'package:pweb/services/auth.dart'; +import 'package:pweb/services/balance.dart'; +import 'package:pweb/services/payments/payment_methods.dart'; +import 'package:pweb/services/payments/upload_history.dart'; +import 'package:pweb/services/recipient/recipient.dart'; +import 'package:pweb/services/wallets.dart'; + + +void _setupLogging() { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + // ignore: avoid_print + print('${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}'); + }); +} + +void main() async { + await Constants.initialize(); + await AmplitudeService.initialize(); + + + _setupLogging(); + setUrlStrategy(PathUrlStrategy()); + + initializeTimeagoLocales(); + + runApp( + MultiProvider( + providers: [ + Provider( + create: (_) => AuthenticationService(), + ), + ChangeNotifierProxyProvider( + create: (context) => TwoFactorProvider( + context.read(), + ), + update: (context, authService, previous) => TwoFactorProvider(authService), + ), + ChangeNotifierProvider(create: (_) => LocaleProvider(null)), + ChangeNotifierProvider(create: (_) => AccountProvider()), + ChangeNotifierProvider(create: (_) => OrganizationsProvider()), + ChangeNotifierProvider(create: (_) => PfeProvider()), + ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), + + ChangeNotifierProvider( + create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(), + ), + ChangeNotifierProvider( + create: (_) => PaymentMethodsProvider(service: MockPaymentMethodsService())..loadMethods(), + ), + ChangeNotifierProvider( + create: (_) => WalletsProvider(MockWalletsService())..loadData(), + ), + ChangeNotifierProvider( + create: (_) => MockPaymentProvider(), + ), + ChangeNotifierProvider( + create: (_) => RecipientProvider(RecipientService())..loadRecipients(), + ), + ChangeNotifierProvider( + create: (context) { + final recipient = context.read(); + final wallets = context.read(); + return PageSelectorProvider( + recipientProvider: recipient, + walletsProvider: wallets, + ); + }, + ), + ChangeNotifierProvider( + create: (_) => BalanceProvider(MockBalanceService())..loadData(), + ), + ], + child: const PayApp(), + ), + ); +} diff --git a/frontend/pweb/lib/models/currency.dart b/frontend/pweb/lib/models/currency.dart new file mode 100644 index 0000000..da336db --- /dev/null +++ b/frontend/pweb/lib/models/currency.dart @@ -0,0 +1 @@ +enum Currency {usd, eur, rub, usdt, usdc} \ No newline at end of file diff --git a/frontend/pweb/lib/models/wallet.dart b/frontend/pweb/lib/models/wallet.dart new file mode 100644 index 0000000..ed376bb --- /dev/null +++ b/frontend/pweb/lib/models/wallet.dart @@ -0,0 +1,38 @@ +import 'package:pweb/models/currency.dart'; + + +class Wallet { + final String id; + final String walletUserID; // ID or number that we show the user + final String name; + final double balance; + final Currency currency; + final bool isHidden; + + Wallet({ + required this.id, + required this.walletUserID, + required this.name, + required this.balance, + required this.currency, + this.isHidden = true, + }); + + Wallet copyWith({ + String? id, + String? name, + double? balance, + Currency? currency, + String? walletUserID, + bool? isHidden, + }) { + return Wallet( + id: id ?? this.id, + name: name ?? this.name, + balance: balance ?? this.balance, + currency: currency ?? this.currency, + walletUserID: walletUserID ?? this.walletUserID, + isHidden: isHidden ?? this.isHidden, + ); + } +} diff --git a/frontend/pweb/lib/pages/2fa/error_message.dart b/frontend/pweb/lib/pages/2fa/error_message.dart new file mode 100644 index 0000000..9806182 --- /dev/null +++ b/frontend/pweb/lib/pages/2fa/error_message.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + + +class ErrorMessage extends StatelessWidget { + final String error; + + const ErrorMessage({super.key, required this.error}); + + @override + Widget build(BuildContext context) => Text( + error, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/2fa/input.dart b/frontend/pweb/lib/pages/2fa/input.dart new file mode 100644 index 0000000..ef8f69b --- /dev/null +++ b/frontend/pweb/lib/pages/2fa/input.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'package:pin_code_fields/pin_code_fields.dart'; + + +class TwoFactorCodeInput extends StatelessWidget { + final void Function(String) onCompleted; + + const TwoFactorCodeInput({super.key, required this.onCompleted}); + + @override + Widget build(BuildContext context) => Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: PinCodeTextField( + length: 6, + appContext: context, + autoFocus: true, + keyboardType: TextInputType.number, + animationType: AnimationType.fade, + pinTheme: PinTheme( + shape: PinCodeFieldShape.box, + borderRadius: BorderRadius.circular(4), + fieldHeight: 48, + fieldWidth: 40, + inactiveColor: Theme.of(context).dividerColor, + activeColor: Theme.of(context).colorScheme.primary, + selectedColor: Theme.of(context).colorScheme.primary, + ), + onCompleted: onCompleted, + onChanged: (_) {}, + ), + ), + ); +} diff --git a/frontend/pweb/lib/pages/2fa/page.dart b/frontend/pweb/lib/pages/2fa/page.dart new file mode 100644 index 0000000..05c9b77 --- /dev/null +++ b/frontend/pweb/lib/pages/2fa/page.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/2fa/error_message.dart'; +import 'package:pweb/pages/2fa/input.dart'; +import 'package:pweb/pages/2fa/prompt.dart'; +import 'package:pweb/pages/2fa/resend.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + +import 'package:provider/provider.dart'; +import 'package:pweb/providers/two_factor.dart'; + + + +class TwoFactorCodePage extends StatelessWidget { + final VoidCallback onVerificationSuccess; + + const TwoFactorCodePage({ + super.key, + required this.onVerificationSuccess, + }); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + if (provider.verificationSuccess) { + WidgetsBinding.instance.addPostFrameCallback((_) { + onVerificationSuccess(); + }); + } + + return Scaffold( + appBar: AppBar(title: const Text('')), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const TwoFactorPromptText(), + const SizedBox(height: 32), + TwoFactorCodeInput( + onCompleted: (code) => provider.submitCode(code), + ), + const SizedBox(height: 24), + if (provider.isSubmitting) + const Center(child: CircularProgressIndicator()) + else + const ResendCodeButton(), + if (provider.hasError) ...[ + const SizedBox(height: 12), + ErrorMessage(error: AppLocalizations.of(context)!.twoFactorError), + ], + ], + ), + ), + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/2fa/prompt.dart b/frontend/pweb/lib/pages/2fa/prompt.dart new file mode 100644 index 0000000..91f4923 --- /dev/null +++ b/frontend/pweb/lib/pages/2fa/prompt.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class TwoFactorPromptText extends StatelessWidget { + const TwoFactorPromptText({super.key}); + + @override + Widget build(BuildContext context) => Text( + AppLocalizations.of(context)!.twoFactorPrompt, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ); +} diff --git a/frontend/pweb/lib/pages/2fa/resend.dart b/frontend/pweb/lib/pages/2fa/resend.dart new file mode 100644 index 0000000..57bde0d --- /dev/null +++ b/frontend/pweb/lib/pages/2fa/resend.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ResendCodeButton extends StatelessWidget { + const ResendCodeButton({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final localizations = AppLocalizations.of(context)!; + + return TextButton( + onPressed: () { + // TODO: Add resend logic + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(0, 0), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + alignment: Alignment.centerLeft, + foregroundColor: theme.colorScheme.primary, + textStyle: theme.textTheme.bodyMedium?.copyWith( + decoration: TextDecoration.underline, + ), + ), + child: Text(localizations.twoFactorResend), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/address_book/form/method_tile.dart b/frontend/pweb/lib/pages/address_book/form/method_tile.dart new file mode 100644 index 0000000..bb5ca51 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/method_tile.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/form.dart'; +import 'package:pweb/pages/payment_methods/icon.dart'; + + +class AdressBookPaymentMethodTile extends StatefulWidget { + final PaymentType type; + final String title; + final Map methods; + final ValueChanged onChanged; + + final double spacingM; + final double spacingS; + final double sizeM; + final TextStyle? titleTextStyle; + + const AdressBookPaymentMethodTile({ + super.key, + required this.type, + required this.title, + required this.methods, + required this.onChanged, + this.spacingM = 12, + this.spacingS = 8, + this.sizeM = 20, + this.titleTextStyle, + }); + + @override + State createState() => _AdressBookPaymentMethodTileState(); +} + +class _AdressBookPaymentMethodTileState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isAdded = widget.methods.containsKey(widget.type); + + return ExpansionTile( + title: Row( + children: [ + Icon( + iconForPaymentType(widget.type), + size: widget.sizeM, + color: isAdded + ? theme.colorScheme.primary + : theme.colorScheme.onSurface, + ), + SizedBox(width: widget.spacingS), + Text( + widget.title, + style: widget.titleTextStyle ?? + theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: isAdded ? theme.colorScheme.primary : null, + ), + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isAdded) + IconButton( + icon: Icon(Icons.delete, color: theme.colorScheme.error), + onPressed: () { + widget.onChanged(null); + }, + ), + Icon( + isAdded ? Icons.check_circle : Icons.add_circle_outline, + color: isAdded ? theme.colorScheme.primary : null, + ), + ], + ), + children: [ + PaymentMethodForm( + key: ValueKey(widget.type), + selectedType: widget.type, + initialData: widget.methods[widget.type], + onChanged: widget.onChanged, + ), + SizedBox(height: widget.spacingM), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/page.dart b/frontend/pweb/lib/pages/address_book/form/page.dart new file mode 100644 index 0000000..a970ca8 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/page.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/card.dart'; +import 'package:pshared/models/payment/methods/iban.dart'; +import 'package:pshared/models/payment/methods/russian_bank.dart'; +import 'package:pshared/models/payment/methods/wallet.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/models/recipient/status.dart'; +import 'package:pshared/models/recipient/type.dart'; + +import 'package:pweb/pages/address_book/form/view.dart'; +import 'package:pweb/services/amplitude.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class AdressBookRecipientForm extends StatefulWidget { + final Recipient? recipient; + final ValueChanged? onSaved; + + const AdressBookRecipientForm({super.key, this.recipient, this.onSaved}); + + @override + State createState() => _AdressBookRecipientFormState(); +} + +class _AdressBookRecipientFormState extends State { + final _formKey = GlobalKey(); + late TextEditingController _nameCtrl; + late TextEditingController _emailCtrl; + RecipientType _type = RecipientType.internal; + RecipientStatus _status = RecipientStatus.ready; + final Map _methods = {}; + + @override + void initState() { + super.initState(); + final r = widget.recipient; + _nameCtrl = TextEditingController(text: r?.name ?? ""); + _emailCtrl = TextEditingController(text: r?.email ?? ""); + _type = r?.type ?? RecipientType.internal; + _status = r?.status ?? RecipientStatus.ready; + + if (r?.card != null) _methods[PaymentType.card] = r!.card; + if (r?.iban != null) _methods[PaymentType.iban] = r!.iban; + if (r?.wallet != null) _methods[PaymentType.wallet] = r!.wallet; + if (r?.bank != null) _methods[PaymentType.bankAccount] = r!.bank; + } + + //TODO Change when registration is ready + void _save() { + if (!_formKey.currentState!.validate() || _methods.isEmpty) { + AmplitudeService.recipientAddCompleted( + _type, + _status, + _methods.keys.toSet(), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context)!.recipientFormRule), + ), + ); + return; + } + + final recipient = Recipient( + name: _nameCtrl.text, + email: _emailCtrl.text, + type: _type, + status: _status, + avatarUrl: null, + card: _methods[PaymentType.card] as CardPaymentMethod?, + iban: _methods[PaymentType.iban] as IbanPaymentMethod?, + wallet: _methods[PaymentType.wallet] as WalletPaymentMethod?, + bank: _methods[PaymentType.bankAccount] as RussianBankAccountPaymentMethod?, + ); + + widget.onSaved?.call(recipient); + } + + @override + Widget build(BuildContext context) { + return FormView( + formKey: _formKey, + nameCtrl: _nameCtrl, + emailCtrl: _emailCtrl, + type: _type, + status: _status, + methods: _methods, + onTypeChanged: (t) => setState(() => _type = t), + onStatusChanged: (s) => setState(() => _status = s), + onMethodsChanged: (type, data) { + setState(() { + if (data != null) { + _methods[type] = data; + } else { + _methods.remove(type); + } + }); + }, + onSave: _save, + isEditing: widget.recipient != null, + onBack: () { + widget.onSaved?.call(null); + }, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/address_book/form/view.dart b/frontend/pweb/lib/pages/address_book/form/view.dart new file mode 100644 index 0000000..c49a8f5 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/view.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/status.dart'; +import 'package:pshared/models/recipient/type.dart'; + +import 'package:pweb/utils/payment/label.dart'; +import 'package:pweb/pages/address_book/form/method_tile.dart'; +import 'package:pweb/pages/address_book/form/widgets/button.dart'; +import 'package:pweb/pages/address_book/form/widgets/email_field.dart'; +import 'package:pweb/pages/address_book/form/widgets/header.dart'; +import 'package:pweb/pages/address_book/form/widgets/name_field.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class FormView extends StatelessWidget { + final GlobalKey formKey; + final TextEditingController nameCtrl; + final TextEditingController emailCtrl; + final RecipientType type; + final RecipientStatus status; + final Map methods; + final ValueChanged onTypeChanged; + final ValueChanged onStatusChanged; + final void Function(PaymentType, Object?) onMethodsChanged; + final VoidCallback onSave; + final bool isEditing; + final VoidCallback onBack; + + final double maxWidth; + final double elevation; + final double borderRadius; + final EdgeInsetsGeometry padding; + final double spacingHeader; + final double spacingFields; + final double spacingDivider; + final double spacingSave; + final double spacingBottom; + final TextStyle? titleTextStyle; + + const FormView({ + super.key, + required this.formKey, + required this.nameCtrl, + required this.emailCtrl, + required this.type, + required this.status, + required this.methods, + required this.onTypeChanged, + required this.onStatusChanged, + required this.onMethodsChanged, + required this.onSave, + required this.isEditing, + required this.onBack, + this.maxWidth = 500, + this.elevation = 4, + this.borderRadius = 16, + this.padding = const EdgeInsets.all(20), + this.spacingHeader = 20, + this.spacingFields = 12, + this.spacingDivider = 40, + this.spacingSave = 30, + this.spacingBottom = 16, + this.titleTextStyle, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Material( + elevation: elevation, + borderRadius: BorderRadius.circular(borderRadius), + color: theme.colorScheme.onSecondary, + child: Padding( + padding: padding, + child: Form( + key: formKey, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HeaderWidget( + isEditing: isEditing, + onBack: onBack, + ), + SizedBox(height: spacingHeader), + NameField(controller: nameCtrl), + SizedBox(height: spacingFields), + EmailField(controller: emailCtrl), + Divider(height: spacingDivider), + Text( + AppLocalizations.of(context)!.choosePaymentMethod, + style: titleTextStyle ?? + theme.textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + SizedBox(height: spacingFields), + ...PaymentType.values.map( + (p) => AdressBookPaymentMethodTile( + type: p, + title: getPaymentTypeLabel(context, p), + methods: methods, + onChanged: (data) => onMethodsChanged(p, data), + ), + ), + SizedBox(height: spacingSave), + SaveButton(onSave: onSave), + SizedBox(height: spacingBottom), + ], + ), + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/button.dart b/frontend/pweb/lib/pages/address_book/form/widgets/button.dart new file mode 100644 index 0000000..540d023 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SaveButton extends StatelessWidget { + final VoidCallback onSave; + + final double width; + final double height; + final double borderRadius; + final String? text; + final TextStyle? textStyle; + + const SaveButton({ + super.key, + required this.onSave, + this.width = 200, + this.height = 45, + this.borderRadius = 12, + this.text, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: SizedBox( + width: width, + height: height, + child: InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: onSave, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Center( + child: Text( + text ?? AppLocalizations.of(context)!.saveRecipient, + style: textStyle ?? + theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/choice_chips.dart b/frontend/pweb/lib/pages/address_book/form/widgets/choice_chips.dart new file mode 100644 index 0000000..fe65c91 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/choice_chips.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + + +class ChoiceChips extends StatelessWidget { + final String label; + final List values; + final T selected; + final ValueChanged onChanged; + + final double spacing; + final double runSpacing; + final double labelSpacing; + + const ChoiceChips({ + super.key, + required this.label, + required this.values, + required this.selected, + required this.onChanged, + this.spacing = 8, + this.runSpacing = 8, + this.labelSpacing = 8, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold), + ), + SizedBox(height: labelSpacing), + Wrap( + spacing: spacing, + runSpacing: runSpacing, + children: values.map((v) { + final isSelected = v == selected; + return ChoiceChip( + selectedColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.onSecondary, + showCheckmark: false, + label: Text( + v.toString().split('.').last, + style: TextStyle( + color: isSelected + ? theme.colorScheme.onSecondary + : theme.colorScheme.inverseSurface, + ), + ), + selected: isSelected, + onSelected: (_) => onChanged(v), + ); + }).toList(), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/email_field.dart b/frontend/pweb/lib/pages/address_book/form/widgets/email_field.dart new file mode 100644 index 0000000..6def3e2 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/email_field.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class EmailField extends StatelessWidget { + final TextEditingController controller; + + final double borderRadius; + final EdgeInsetsGeometry contentPadding; + + const EmailField({ + super.key, + required this.controller, + this.borderRadius = 12, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + }); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; + + return TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: loc.username, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius), + ), + contentPadding: contentPadding, + ), + validator: (v) => + v == null || v.isEmpty ? loc.usernameErrorInvalid : null, + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/header.dart b/frontend/pweb/lib/pages/address_book/form/widgets/header.dart new file mode 100644 index 0000000..0585999 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/header.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class HeaderWidget extends StatelessWidget { + final bool isEditing; + final VoidCallback? onBack; + + final double spacing; + final TextStyle? textStyle; + + const HeaderWidget({ + super.key, + required this.isEditing, + this.onBack, + this.spacing = 8, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + color: theme.colorScheme.primary, + onPressed: onBack, + ), + SizedBox(width: spacing), + Text( + isEditing ? l10n.editRecipient : l10n.addRecipient, + style: textStyle ?? + theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/name_field.dart b/frontend/pweb/lib/pages/address_book/form/widgets/name_field.dart new file mode 100644 index 0000000..9c1f5f5 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/name_field.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class NameField extends StatelessWidget { + final TextEditingController controller; + + final double borderRadius; + final EdgeInsetsGeometry contentPadding; + + const NameField({ + super.key, + required this.controller, + this.borderRadius = 12, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + }); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; + + return TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: loc.recipientName, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadius), + ), + contentPadding: contentPadding, + ), + validator: (v) => v == null || v.isEmpty ? loc.enterRecipientName : null, + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/page/filter_button.dart b/frontend/pweb/lib/pages/address_book/page/filter_button.dart new file mode 100644 index 0000000..b4f1a80 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/filter_button.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/filter.dart'; + + +class RecipientFilterButton extends StatelessWidget { + final String text; + final RecipientFilter filter; + final RecipientFilter selected; + final ValueChanged onTap; + + const RecipientFilterButton({ + super.key, + required this.text, + required this.filter, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final isSelected = selected == filter; + final theme = Theme.of(context).colorScheme; + + return ElevatedButton( + onPressed: () => onTap(filter), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Colors.transparent), + overlayColor: WidgetStateProperty.all(Colors.transparent), + shadowColor: WidgetStateProperty.all(Colors.transparent), + elevation: WidgetStateProperty.all(0), + ), + child: IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + text, + style: TextStyle( + fontSize: 20, + color: isSelected + ? theme.onPrimaryContainer + : theme.onPrimaryContainer.withAlpha(60), + ), + ), + SizedBox( + height: 2, + child: DecoratedBox( + decoration: BoxDecoration( + color: isSelected + ? theme.primary + : theme.onPrimaryContainer.withAlpha(60), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/page/header.dart b/frontend/pweb/lib/pages/address_book/page/header.dart new file mode 100644 index 0000000..84d033b --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/header.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class RecipientAddressBookHeader extends StatelessWidget { + final VoidCallback onAddRecipient; + + const RecipientAddressBookHeader({super.key, required this.onAddRecipient}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + final l10 = AppLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + l10.recipients, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + TextButton.icon( + onPressed: onAddRecipient, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(theme.primary), + shadowColor: WidgetStateProperty.all(theme.onPrimaryContainer), + elevation: WidgetStateProperty.all(2), + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + ), + icon: Icon(Icons.add, color: theme.onSecondary), + label: Text(l10.addRecipient, style: TextStyle(color: theme.onSecondary)), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/address_book/page/list.dart b/frontend/pweb/lib/pages/address_book/page/list.dart new file mode 100644 index 0000000..1555dc3 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/list.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/address_book/page/recipient/item.dart'; + + +class RecipientAddressBookList extends StatelessWidget { + final List filteredRecipients; + final ValueChanged? onSelected; + final ValueChanged? onEdit; + final ValueChanged? onDelete; + + const RecipientAddressBookList({ + super.key, + required this.filteredRecipients, + this.onSelected, + this.onEdit, + this.onDelete, + }); + + @override +Widget build(BuildContext context) { + return ListView.builder( + itemCount: filteredRecipients.length, + itemBuilder: (context, index) { + final recipient = filteredRecipients[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: RecipientAddressBookItem( + recipient: recipient, + onTap: () => onSelected?.call(recipient), + onEdit: () => onEdit?.call(recipient), + onDelete: () => onDelete?.call(recipient), + ), + ); + }, + ); +} +} diff --git a/frontend/pweb/lib/pages/address_book/page/page.dart b/frontend/pweb/lib/pages/address_book/page/page.dart new file mode 100644 index 0000000..e459bba --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/page.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pshared/models/recipient/filter.dart'; +import 'package:pweb/pages/address_book/page/filter_button.dart'; +import 'package:pweb/pages/address_book/page/header.dart'; +import 'package:pweb/pages/address_book/page/list.dart'; +import 'package:pweb/pages/address_book/page/search.dart'; +import 'package:pweb/providers/recipient.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class RecipientAddressBookPage extends StatelessWidget { + final ValueChanged onRecipientSelected; + final VoidCallback onAddRecipient; + final ValueChanged? onEditRecipient; + + const RecipientAddressBookPage({ + super.key, + required this.onRecipientSelected, + required this.onAddRecipient, + this.onEditRecipient, + }); + + static const double _expandedHeight = 550; + static const double _paddingAll = 16; + static const double _bigBox = 30; + static const double _smallBox = 20; + + + @override + Widget build(BuildContext context) { + + final loc = AppLocalizations.of(context)!; + final provider = context.watch(); + + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); //TODO This should be in the provider + } + + if (provider.error != null) { + return Center(child: Text('Error: ${provider.error}')); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RecipientAddressBookHeader(onAddRecipient: onAddRecipient), + const SizedBox(height: _smallBox), + RecipientSearchField( + controller: TextEditingController(text: provider.query), + focusNode: FocusNode(), + onChanged: provider.setQuery, + ), + const SizedBox(height: _bigBox), + Row( + children: [ + RecipientFilterButton( + text: loc.allStatus, + filter: RecipientFilter.all, + selected: provider.selectedFilter, + onTap: provider.setFilter, + ), + RecipientFilterButton( + text: loc.readyStatus, + filter: RecipientFilter.ready, + selected: provider.selectedFilter, + onTap: provider.setFilter, + ), + RecipientFilterButton( + text: loc.registeredStatus, + filter: RecipientFilter.registered, + selected: provider.selectedFilter, + onTap: provider.setFilter, + ), + RecipientFilterButton( + text: loc.notRegisteredStatus, + filter: RecipientFilter.notRegistered, + selected: provider.selectedFilter, + onTap: provider.setFilter, + ), + ], + ), + SizedBox( + height: _expandedHeight, + child: Padding( + padding: const EdgeInsets.all(_paddingAll), + child: RecipientAddressBookList( + filteredRecipients: provider.filteredRecipients, + onEdit: (recipient) => onEditRecipient?.call(recipient), + onSelected: onRecipientSelected, + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/address_book/page/recipient/actions.dart b/frontend/pweb/lib/pages/address_book/page/recipient/actions.dart new file mode 100644 index 0000000..8007a83 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/recipient/actions.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + + +class RecipientActions extends StatelessWidget { + final VoidCallback onEdit; + final VoidCallback onDelete; + + const RecipientActions({super.key, required this.onEdit, required this.onDelete}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + IconButton(icon: Icon(Icons.edit, color: Theme.of(context).colorScheme.primary), onPressed: onEdit), + IconButton(icon: Icon(Icons.delete, color: Theme.of(context).colorScheme.error), onPressed: onDelete), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/page/recipient/info_column.dart b/frontend/pweb/lib/pages/address_book/page/recipient/info_column.dart new file mode 100644 index 0000000..64e941a --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/recipient/info_column.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + + +class RecipientInfoColumn extends StatelessWidget { + final String name; + final String email; + + const RecipientInfoColumn({super.key, required this.name, required this.email}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 19)), + Text(email, style: Theme.of(context).textTheme.bodyMedium), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/page/recipient/info_row.dart b/frontend/pweb/lib/pages/address_book/page/recipient/info_row.dart new file mode 100644 index 0000000..869088e --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/recipient/info_row.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/icon.dart'; +import 'package:pweb/utils/payment/label.dart'; + + +class RecipientAddressBookInfoRow extends StatelessWidget { + final PaymentType type; + final String value; + + final double spacingWidth; + final double spacingHeight; + final double iconSize; + final double titleFontSize; + final double valueFontSize; + final TextStyle? textStyle; + + const RecipientAddressBookInfoRow({ + super.key, + required this.type, + required this.value, + this.spacingWidth = 8.0, + this.spacingHeight = 2.0, + this.iconSize = 20.0, + this.titleFontSize = 16.0, + this.valueFontSize = 12.0, + this.textStyle, + }); + + @override + Widget build(BuildContext context) { + final style = textStyle ?? Theme.of(context).textTheme.bodySmall!; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(iconForPaymentType(type), size: iconSize), + SizedBox(width: spacingWidth), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getPaymentTypeLabel(context, type), + style: style.copyWith(fontSize: titleFontSize), + ), + SizedBox(height: spacingHeight), + Text( + value, + style: style.copyWith(fontSize: valueFontSize), + ), + ], + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/address_book/page/recipient/item.dart b/frontend/pweb/lib/pages/address_book/page/recipient/item.dart new file mode 100644 index 0000000..f8b3917 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/recipient/item.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/address_book/page/recipient/actions.dart'; +import 'package:pweb/pages/address_book/page/recipient/info_column.dart'; +import 'package:pweb/pages/address_book/page/recipient/payment_row.dart'; +import 'package:pweb/pages/address_book/page/recipient/status.dart'; +import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart'; + + +class RecipientAddressBookItem extends StatefulWidget { + final Recipient recipient; + final VoidCallback onTap; + final VoidCallback onEdit; + final VoidCallback onDelete; + + final double borderRadius; + final double elevation; + final EdgeInsetsGeometry padding; + final double spacingDotAvatar; + final double spacingAvatarInfo; + final double spacingBottom; + final double avatarRadius; + + const RecipientAddressBookItem({ + super.key, + required this.recipient, + required this.onTap, + required this.onEdit, + required this.onDelete, + this.borderRadius = 12, + this.elevation = 4, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + this.spacingDotAvatar = 8, + this.spacingAvatarInfo = 16, + this.spacingBottom = 10, + this.avatarRadius = 24, + }); + + @override + State createState() => _RecipientAddressBookItemState(); +} + +class _RecipientAddressBookItemState extends State { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + final recipient = widget.recipient; + + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: InkWell( + onTap: widget.onTap, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(widget.borderRadius)), + elevation: widget.elevation, + color: Theme.of(context).colorScheme.onSecondary, + child: Padding( + padding: widget.padding, + child: Column( + children: [ + Row( + children: [ + RecipientStatusDot(status: recipient.status), + SizedBox(width: widget.spacingDotAvatar), + RecipientAvatar( + name: recipient.name, + avatarUrl: recipient.avatarUrl, + isVisible: false, + avatarRadius: widget.avatarRadius, + ), + SizedBox(width: widget.spacingAvatarInfo), + Expanded( + child: RecipientInfoColumn( + name: recipient.name, + email: recipient.email, + ), + ), + if (_isHovered) + RecipientActions( + onEdit: widget.onEdit, onDelete: widget.onDelete), + ], + ), + SizedBox(height: widget.spacingBottom), + RecipientPaymentRow(recipient: recipient), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/page/recipient/payment_row.dart b/frontend/pweb/lib/pages/address_book/page/recipient/payment_row.dart new file mode 100644 index 0000000..1cd3aaf --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/recipient/payment_row.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/address_book/page/recipient/info_row.dart'; + + +class RecipientPaymentRow extends StatelessWidget { + final Recipient recipient; + final double spacing; + + const RecipientPaymentRow({ + super.key, + required this.recipient, + this.spacing = 18 + }); + + @override + Widget build(BuildContext context) { + return Row( + spacing: spacing, + children: [ + if (recipient.bank?.accountNumber.isNotEmpty ?? false) + RecipientAddressBookInfoRow( + type: PaymentType.bankAccount, + value: recipient.bank!.accountNumber + ), + if (recipient.card?.pan.isNotEmpty ?? false) + RecipientAddressBookInfoRow( + type: PaymentType.card, + value: recipient.card!.pan + ), + if (recipient.iban?.iban.isNotEmpty ?? false) + RecipientAddressBookInfoRow( + type: PaymentType.iban, + value: recipient.iban!.iban + ), + if (recipient.wallet?.walletId.isNotEmpty ?? false) + RecipientAddressBookInfoRow( + type: PaymentType.wallet, + value: recipient.wallet!.walletId + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/address_book/page/recipient/status.dart b/frontend/pweb/lib/pages/address_book/page/recipient/status.dart new file mode 100644 index 0000000..a9a7db3 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/recipient/status.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/status.dart'; + + +class RecipientStatusDot extends StatelessWidget { + final RecipientStatus status; + + const RecipientStatusDot({super.key, required this.status}); + + @override + Widget build(BuildContext context) { + Color color; + switch (status) { + case RecipientStatus.ready: + color = Colors.green; + break; + case RecipientStatus.notRegistered: + color = Theme.of(context).colorScheme.error; + break; + case RecipientStatus.registered: + color = Colors.yellow; + break; + } + + return Container( + width: 12, + height: 12, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/page/search.dart b/frontend/pweb/lib/pages/address_book/page/search.dart new file mode 100644 index 0000000..5d00c25 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/page/search.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class RecipientSearchField extends StatelessWidget { + final TextEditingController controller; + final ValueChanged onChanged; + final FocusNode? focusNode; + + const RecipientSearchField({ + super.key, + required this.controller, + required this.onChanged, + this.focusNode, + }); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return TextField( + controller: controller, + focusNode: focusNode, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + hintText: l10n.searchHint, + border: const OutlineInputBorder(), + fillColor: Theme.of(context).colorScheme.onSecondary, + filled: true, + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.clear(); + onChanged(''); + focusNode?.unfocus(); + }, + ), + ), + onChanged: onChanged, + ); + + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart new file mode 100644 index 0000000..49f4658 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + + +class BalanceAddFunds extends StatelessWidget { + final VoidCallback onTopUp; + + const BalanceAddFunds({ + super.key, + required this.onTopUp, + }); + + static const double _borderRadius = 5.0; + static const double _iconSize = 24.0; + static const double _paddingVertical = 2.0; + static const double _spacingSmall = 3.0; + static const double _spacingMedium = 5.0; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + + return InkWell( + onTap: onTopUp, + borderRadius: BorderRadius.circular(_borderRadius), + hoverColor: colorScheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: _paddingVertical), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: _spacingSmall), + Icon( + Icons.add_circle, + color: colorScheme.primary, + size: _iconSize, + ), + const SizedBox(width: _spacingMedium), + Text( + 'Add funds', + style: textTheme.bodyMedium?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: _spacingSmall), + ], + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart new file mode 100644 index 0000000..409a59b --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/utils/currency.dart'; + + +class BalanceAmount extends StatelessWidget { + final Wallet wallet; + final VoidCallback onToggleVisibility; + + const BalanceAmount({ + super.key, + required this.wallet, + required this.onToggleVisibility, + }); + + static const double _iconSpacing = 12.0; + static const double _iconSize = 24.0; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + final currencyBalance = currencyCodeToSymbol(wallet.currency); + + return Row( + children: [ + Text( + wallet.isHidden ? '•••• $currencyBalance' : '${wallet.balance.toStringAsFixed(2)} $currencyBalance', + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(width: _iconSpacing), + GestureDetector( + onTap: onToggleVisibility, + child: Icon( + wallet.isHidden ? Icons.visibility_off : Icons.visibility, + size: _iconSize, + color: colorScheme.onSurface, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart new file mode 100644 index 0000000..6f2fb87 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart'; +import 'package:pweb/providers/wallets.dart'; + + +class BalanceWidget extends StatelessWidget { + const BalanceWidget({super.key}); + + @override + Widget build(BuildContext context) { + final walletsProvider = context.watch(); + + if (walletsProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final wallets = walletsProvider.wallets; + + if (wallets == null || wallets.isEmpty) { + return const Center(child: Text('No wallets available')); + } + + return + WalletCarousel( + wallets: wallets, + onWalletChanged: walletsProvider.selectWallet, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart new file mode 100644 index 0000000..3586bd8 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; +import 'package:pweb/providers/wallets.dart'; + + +class WalletCard extends StatelessWidget { + final Wallet wallet; + + const WalletCard({ + super.key, + required this.wallet, + }); + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).colorScheme.onSecondary, + elevation: WalletCardConfig.elevation, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), + ), + child: Padding( + padding: WalletCardConfig.contentPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BalanceHeader( + walletName: wallet.name, + walletId: wallet.walletUserID, + ), + BalanceAmount( + wallet: wallet, + onToggleVisibility: () { + context.read().toggleVisibility(wallet.id); + }, + ), + BalanceAddFunds( + onTopUp: () { + // TODO: Implement top-up functionality + }, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart new file mode 100644 index 0000000..426cd7b --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/card.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; +import 'package:pweb/providers/carousel.dart'; + + +class WalletCarousel extends StatefulWidget { + final List wallets; + final ValueChanged onWalletChanged; + + const WalletCarousel({ + super.key, + required this.wallets, + required this.onWalletChanged, + }); + + @override + State createState() => _WalletCarouselState(); +} + +class _WalletCarouselState extends State { + late final PageController _pageController; + int _currentPage = 0; + + @override + void initState() { + super.initState(); + _pageController = PageController( + viewportFraction: WalletCardConfig.viewportFraction, + ); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onPageChanged(int index) { + setState(() { + _currentPage = index; + }); + context.read().updateIndex(index); + widget.onWalletChanged(widget.wallets[index]); + } + + void _goToPreviousPage() { + if (_currentPage > 0) { + _pageController.animateToPage( + _currentPage - 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + void _goToNextPage() { + if (_currentPage < widget.wallets.length - 1) { + _pageController.animateToPage( + _currentPage + 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: WalletCardConfig.cardHeight, + child: PageView.builder( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.wallets.length, + onPageChanged: _onPageChanged, + itemBuilder: (context, index) { + return Padding( + padding: WalletCardConfig.cardPadding, + child: WalletCard(wallet: widget.wallets[index]), + ); + }, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: _currentPage > 0 ? _goToPreviousPage : null, + icon: const Icon(Icons.arrow_back), + ), + const SizedBox(width: 16), + CarouselIndicator(itemCount: widget.wallets.length), + const SizedBox(width: 16), + IconButton( + onPressed: _currentPage < widget.wallets.length - 1 + ? _goToNextPage + : null, + icon: const Icon(Icons.arrow_forward), + ), + ], + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart new file mode 100644 index 0000000..135895d --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + + +abstract class WalletCardConfig { + static const double cardHeight = 130.0; + static const double elevation = 4.0; + static const double borderRadius = 16.0; + static const double viewportFraction = 0.9; + + static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 8); + static const EdgeInsets contentPadding = EdgeInsets.all(16); + + static const double dotSize = 8.0; + static const EdgeInsets dotMargin = EdgeInsets.symmetric(horizontal: 4); +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart new file mode 100644 index 0000000..48c5090 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + + +class BalanceHeader extends StatelessWidget { + final String walletName; + final String walletId; + + const BalanceHeader({ + super.key, + required this.walletName, + required this.walletId, + }); + + static const double _spacing = 8.0; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + + return Row( + children: [ + Text( + walletName, + style: textTheme.titleMedium?.copyWith( + color: colorScheme.onSurface, + ), + ), + const SizedBox(width: _spacing), + Text( + walletId, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurface, + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/indicator.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/indicator.dart new file mode 100644 index 0000000..d28c8c2 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/indicator.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; + +import 'package:pweb/providers/carousel.dart'; + + +class CarouselIndicator extends StatelessWidget { + final int itemCount; + + const CarouselIndicator({ + super.key, + required this.itemCount, + }); + + @override + Widget build(BuildContext context) { + final currentIndex = context.watch().currentIndex; + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + itemCount, + (index) => _Dot(isActive: currentIndex == index), + ), + ); + } +} + +class _Dot extends StatelessWidget { + final bool isActive; + + const _Dot({required this.isActive}); + + @override + Widget build(BuildContext context) { + return Container( + width: WalletCardConfig.dotSize, + height: WalletCardConfig.dotSize, + margin: WalletCardConfig.dotMargin, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isActive + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.primary.withAlpha(60), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/buttons.dart b/frontend/pweb/lib/pages/dashboard/buttons/buttons.dart new file mode 100644 index 0000000..a32b9d6 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/buttons.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + + +class TransactionRefButton extends StatelessWidget { + final VoidCallback onTap; + final bool isActive; + final String label; + final IconData icon; + + const TransactionRefButton({ + super.key, + required this.onTap, + required this.isActive, + required this.label, + required this.icon, + }); + + static const double _horizontalPadding = 10.0; + static const double _verticalPadding = 5.0; + static const double _iconSize = 24.0; + static const double _spacing = 10.0; + static const double _borderRadius = 12.0; + static const FontWeight _fontWeight = FontWeight.w400; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + + final backgroundColor = isActive ? theme.primary : theme.onSecondary; + final foregroundColor = isActive ? theme.onPrimary : theme.onPrimaryContainer; + final hoverColor = isActive ? theme.primary : theme.secondaryContainer; + + return Material( + color: backgroundColor, + elevation: 4, + borderRadius: BorderRadius.circular(_borderRadius), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(_borderRadius), + hoverColor: hoverColor, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: _horizontalPadding, + vertical: _verticalPadding, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: _fontWeight, + color: foregroundColor, + ), + ), + const SizedBox(width: _spacing), + Icon( + icon, + color: foregroundColor, + size: _iconSize, + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/dashboard.dart b/frontend/pweb/lib/pages/dashboard/dashboard.dart new file mode 100644 index 0000000..55d8690 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/dashboard.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/balance.dart'; +import 'package:pweb/pages/dashboard/buttons/buttons.dart'; +import 'package:pweb/pages/dashboard/payouts/multiple/title.dart'; +import 'package:pweb/pages/dashboard/payouts/multiple/widget.dart'; +import 'package:pweb/pages/dashboard/payouts/single/widget.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class AppSpacing { + static const double small = 10; + static const double medium = 16; + static const double large = 20; +} + +class DashboardPage extends StatefulWidget { + final ValueChanged onRecipientSelected; + final void Function(PaymentType type) onGoToPaymentWithoutRecipient; + + const DashboardPage({ + super.key, + required this.onRecipientSelected, + required this.onGoToPaymentWithoutRecipient, + }); + + @override + State createState() => _DashboardPageState(); +} + +class _DashboardPageState extends State { + bool _showContainerSingle = true; + bool _showContainerMultiple = false; + + void _setActive(bool single) { + setState(() { + _showContainerSingle = single; + _showContainerMultiple = !single; + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + flex: 0, + child: TransactionRefButton( + onTap: () => _setActive(true), + isActive: _showContainerSingle, + label: AppLocalizations.of(context)!.sendSingle, + icon: Icons.person_add, + ), + ), + const SizedBox(width: AppSpacing.small), + Expanded( + flex: 0, + child: TransactionRefButton( + onTap: () => _setActive(false), + isActive: _showContainerMultiple, + label: AppLocalizations.of(context)!.sendMultiple, + icon: Icons.group_add, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.medium), + BalanceWidget(), + const SizedBox(height: AppSpacing.small), + if (_showContainerMultiple) TitleMultiplePayout(), + const SizedBox(height: AppSpacing.medium), + if (_showContainerSingle) + SinglePayoutForm( + onRecipientSelected: widget.onRecipientSelected, + onGoToPayment: widget.onGoToPaymentWithoutRecipient, + ), + if (_showContainerMultiple) MultiplePayoutForm(), + ], + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/organization/button.dart b/frontend/pweb/lib/pages/dashboard/organization/button.dart new file mode 100644 index 0000000..62b4115 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/organization/button.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + + +class OrganizationButton extends StatelessWidget { + const OrganizationButton({super.key}); + + @override + Widget build(BuildContext context) => IconButton( + icon: Icon(Icons.person), + onPressed: null, + ); +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/csv.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/csv.dart new file mode 100644 index 0000000..8a169a4 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/csv.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class UploadCSVSection extends StatelessWidget { + const UploadCSVSection({super.key}); + + static const double _verticalSpacing = 10; + static const double _iconTextSpacing = 5; + static const double _buttonVerticalPadding = 12; + static const double _buttonHorizontalPadding = 24; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.upload), + const SizedBox(width: _iconTextSpacing), + Text( + l10n.uploadCSV, + style: theme.textTheme.bodyLarge?.copyWith( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: _verticalSpacing), + Container( + height: 140, + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.upload_file, size: 36, color: theme.colorScheme.primary), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: _buttonHorizontalPadding, + vertical: _buttonVerticalPadding, + ), + ), + child: Text(l10n.upload), + ), + const SizedBox(height: 8), + Text( + l10n.hintUpload, + style: const TextStyle(fontSize: 12), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/form.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/form.dart new file mode 100644 index 0000000..333a09a --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/form.dart @@ -0,0 +1,13 @@ +class MultiplePayoutRow { + final String token; + final String amount; + final String currency; + final String comment; + + const MultiplePayoutRow({ + required this.token, + required this.amount, + required this.currency, + required this.comment, + }); +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart new file mode 100644 index 0000000..0760430 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + +import 'package:pweb/providers/upload_history.dart'; + + +class UploadHistorySection extends StatelessWidget { + const UploadHistorySection({super.key}); + + static const double _smallBox = 5; + static const double _radius = 6; + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + final theme = Theme.of(context); + final l10 = AppLocalizations.of(context)!; + + + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (provider.error != null) { + return Text("Error: ${provider.error}"); + } + final items = provider.data ?? []; + + return Column( + children: [ + Row( + children: [ + const Icon(Icons.history), + const SizedBox(width: _smallBox), + Text(l10.uploadHistory, style: theme.textTheme.bodyLarge), + ], + ), + DataTable( + columns: [ + DataColumn(label: Text(l10.fileNameColumn)), + DataColumn(label: Text(l10.colStatus)), + DataColumn(label: Text(l10.dateColumn)), + DataColumn(label: Text(l10.details)), + ], + rows: items.map((file) { + final isError = file.status == "Error"; + final statusColor = isError ? Colors.red : Colors.green; + return DataRow( + cells: [ + DataCell(Text(file.name)), + DataCell(Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withAlpha(20), + borderRadius: BorderRadius.circular(_radius), + ), + child: Text(file.status, style: TextStyle(color: statusColor)), + )), + DataCell(Text(file.time)), + DataCell(TextButton(onPressed: () {}, child: Text(l10.showDetails))), + ], + ); + }).toList(), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sample.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sample.dart new file mode 100644 index 0000000..ce39fce --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sample.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/pages/dashboard/payouts/multiple/form.dart'; + + +class FileFormatSampleSection extends StatelessWidget { + const FileFormatSampleSection({super.key}); + + static final List sampleRows = [ + MultiplePayoutRow(token: "d921...161", amount: "500", currency: "RUB", comment: "cashback001"), + MultiplePayoutRow(token: "d921...162", amount: "100", currency: "USD", comment: "cashback002"), + MultiplePayoutRow(token: "d921...163", amount: "120", currency: "EUR", comment: "cashback003"), + ]; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + + final titleStyle = theme.textTheme.bodyLarge?.copyWith( + fontSize: 18, + fontWeight: FontWeight.w600, + ); + + final linkStyle = theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.primary, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const Icon(Icons.filter_list), + const SizedBox(width: 5), + Text(l10n.exampleTitle, style: titleStyle), + ], + ), + const SizedBox(height: 12), + _buildDataTable(l10n), + const SizedBox(height: 10), + TextButton( + onPressed: () {}, + style: TextButton.styleFrom(padding: EdgeInsets.zero), + child: Text(l10n.downloadSampleCSV, style: linkStyle), + ), + ], + ); + } + + Widget _buildDataTable(AppLocalizations l10n) { + return DataTable( + columnSpacing: 20, + columns: [ + DataColumn(label: Text(l10n.tokenColumn)), + DataColumn(label: Text(l10n.amount)), + DataColumn(label: Text(l10n.currency)), + DataColumn(label: Text(l10n.comment)), + ], + rows: sampleRows.map((row) { + return DataRow(cells: [ + DataCell(Text(row.token)), + DataCell(Text(row.amount)), + DataCell(Text(row.currency)), + DataCell(Text(row.comment)), + ]); + }).toList(), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/title.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/title.dart new file mode 100644 index 0000000..aee119c --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/title.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class TitleMultiplePayout extends StatelessWidget { + const TitleMultiplePayout({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.multiplePayout, + style: theme.textTheme.titleLarge, + ), + const SizedBox(width: 20), + Text( + AppLocalizations.of(context)!.howItWorks, + style: theme.textTheme.bodyLarge!.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/widget.dart new file mode 100644 index 0000000..cf3efe6 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/widget.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/dashboard/payouts/multiple/csv.dart'; +import 'package:pweb/pages/dashboard/payouts/multiple/history.dart'; +import 'package:pweb/pages/dashboard/payouts/multiple/sample.dart'; + + +class MultiplePayoutForm extends StatelessWidget { + const MultiplePayoutForm({super.key}); + + static const double _spacing = 12; + static const double _bottomSpacing = 40; + + static final List _cards = const [ + FileFormatSampleSection(), + UploadCSVSection(), + UploadHistorySection(), + ]; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + for (int i = 0; i < _cards.length; i++) ...[ + _StyledCard(child: _cards[i]), + if (i < _cards.length - 1) const SizedBox(height: _spacing), + ], + const SizedBox(height: _bottomSpacing), + ], + ); + } +} + +class _StyledCard extends StatelessWidget { + final Widget child; + const _StyledCard({required this.child}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SizedBox( + width: double.infinity, + child: Card( + margin: const EdgeInsets.all(1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + color: theme.colorScheme.onSecondary, + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/payment_form.dart b/frontend/pweb/lib/pages/dashboard/payouts/payment_form.dart new file mode 100644 index 0000000..4285416 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/payment_form.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/providers/mock_payment.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentFormWidget extends StatelessWidget { + const PaymentFormWidget({super.key}); + + static const double _smallSpacing = 5; + static const double _mediumSpacing = 10; + static const double _largeSpacing = 16; + static const double _extraSpacing = 15; + + String _formatAmount(double amount) => amount.toStringAsFixed(2); + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(loc.details, style: theme.textTheme.titleMedium), + const SizedBox(height: _smallSpacing), + + TextField( + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: loc.amount, + border: const OutlineInputBorder(), + ), + onChanged: (val) { + final parsed = double.tryParse(val.replaceAll(',', '.')) ?? 0.0; + provider.setAmount(parsed); + }, + ), + + const SizedBox(height: _mediumSpacing), + + Row( + spacing: _mediumSpacing, + children: [ + Text(loc.recipientPaysFee, style: theme.textTheme.titleMedium), + Switch( + value: !provider.payerCoversFee, + onChanged: (val) => provider.setPayerCoversFee(!val), + ), + ], + ), + + const SizedBox(height: _largeSpacing), + + Align( + alignment: Alignment.center, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _SummaryRow(label: loc.sentAmount(_formatAmount(provider.amount)), style: theme.textTheme.titleMedium), + _SummaryRow(label: loc.fee(_formatAmount(provider.fee)), style: theme.textTheme.titleMedium), + _SummaryRow(label: loc.recipientWillReceive(_formatAmount(provider.recipientGets)), style: theme.textTheme.titleMedium), + + const SizedBox(height: _extraSpacing), + + _SummaryRow( + label: loc.total(_formatAmount(provider.total)), + style: theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + ), + ], + ); + } +} + +class _SummaryRow extends StatelessWidget { + final String label; + final TextStyle? style; + + const _SummaryRow({required this.label, this.style}); + + @override + Widget build(BuildContext context) { + return Text(label, style: style); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/avatar.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/avatar.dart new file mode 100644 index 0000000..cf4f74b --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/avatar.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/utils/initials.dart'; + + +class RecipientAvatar extends StatelessWidget { + final String name; + final String? avatarUrl; + final double avatarRadius; + final TextStyle? nameStyle; + final bool isVisible; + + static const double _verticalSpacing = 5; + + const RecipientAvatar({ + super.key, + required this.name, + this.avatarUrl, + required this.avatarRadius, + this.nameStyle, + required this.isVisible, + }); + + @override + Widget build(BuildContext context) { + final textColor = Theme.of(context).colorScheme.onPrimary; + + return Column( + children: [ + CircleAvatar( + radius: avatarRadius, + backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, + backgroundColor: Theme.of(context).colorScheme.primary, + child: avatarUrl == null + ? Text( + getInitials(name), + style: TextStyle( + color: textColor, + fontSize: avatarRadius * 0.8, + ), + ) + : null, + ), + const SizedBox(height: _verticalSpacing), + if (isVisible) + Text( + name, + overflow: TextOverflow.ellipsis, + style: nameStyle ?? Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart new file mode 100644 index 0000000..bdcf814 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class PaymentInfoRow extends StatelessWidget { + final String label; + final String value; + + const PaymentInfoRow({ + super.key, + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text(label, style: Theme.of(context).textTheme.bodySmall), + const SizedBox(width: 8), + Text(value, style: Theme.of(context).textTheme.bodySmall), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart new file mode 100644 index 0000000..76c9c74 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/item.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart'; +import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart'; +import 'package:pweb/utils/payment/label.dart'; + + +class RecipientItem extends StatelessWidget { + final Recipient recipient; + final VoidCallback onTap; + + static const double _horizontalPadding = 16.0; + static const double _verticalPadding = 8.0; + static const double _avatarRadius = 20; + static const double _spacingWidth = 12; + + const RecipientItem({ + super.key, + required this.recipient, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: _horizontalPadding, + vertical: _verticalPadding, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: RecipientAvatar( + isVisible: false, + name: recipient.name, + avatarUrl: recipient.avatarUrl, + avatarRadius: _avatarRadius, + nameStyle: Theme.of(context).textTheme.bodyMedium, + ), + title: Text(recipient.name), + subtitle: Text(recipient.email), + ), + ), + const SizedBox(width: _spacingWidth), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (recipient.bank?.accountNumber.isNotEmpty == true) + PaymentInfoRow( + label: getPaymentTypeLabel(context, PaymentType.bankAccount), + value: recipient.bank!.accountNumber, + ), + if (recipient.card?.pan.isNotEmpty == true) + PaymentInfoRow( + label: getPaymentTypeLabel(context, PaymentType.card), + value: recipient.card!.pan, + ), + if (recipient.iban?.iban.isNotEmpty == true) + PaymentInfoRow( + label: getPaymentTypeLabel(context, PaymentType.iban), + value: recipient.iban!.iban, + ), + if (recipient.wallet?.walletId.isNotEmpty == true) + PaymentInfoRow( + label: getPaymentTypeLabel(context, PaymentType.wallet), + value: recipient.wallet!.walletId, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart new file mode 100644 index 0000000..5f9f664 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/item.dart'; + + +class LongListAdressBookPayout extends StatelessWidget { + final List filteredRecipients; + final ValueChanged? onSelected; + + const LongListAdressBookPayout({ + super.key, + required this.filteredRecipients, + this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: filteredRecipients.length, + itemBuilder: (context, index) { + final recipient = filteredRecipients[index]; + return RecipientItem( + recipient: recipient, + onTap: () => onSelected!(recipient), + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/short_list.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/short_list.dart new file mode 100644 index 0000000..8486f1c --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/short_list.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart'; + + +class ShortListAdressBookPayout extends StatelessWidget { + final List recipients; + final ValueChanged onSelected; + + const ShortListAdressBookPayout({ + super.key, + required this.recipients, + required this.onSelected, + }); + + static const double _avatarRadius = 20; + static const double _avatarSize = 80; + static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 10, vertical: 8); + static const TextStyle _nameStyle = TextStyle(fontSize: 12); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: recipients.map((recipient) { + return Padding( + padding: _padding, + child: InkWell( + borderRadius: BorderRadius.circular(5), + hoverColor: Theme.of(context).colorScheme.primaryContainer, + onTap: () => onSelected(recipient), + child: SizedBox( + height: _avatarSize, + width: _avatarSize, + child: RecipientAvatar( + isVisible: true, + name: recipient.name, + avatarUrl: recipient.avatarUrl, + avatarRadius: _avatarRadius, + nameStyle: _nameStyle, + ), + ), + ), + ); + }).toList(), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart new file mode 100644 index 0000000..4974e05 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/address_book/page/search.dart'; +import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/long_list.dart'; +import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart'; +import 'package:pweb/providers/recipient.dart'; + + +class AdressBookPayout extends StatefulWidget { + final ValueChanged onSelected; + + const AdressBookPayout({ + super.key, + required this.onSelected, + }); + + @override + State createState() => _AdressBookPayoutState(); +} + +class _AdressBookPayoutState extends State { + static const double _expandedHeight = 400; + static const double _collapsedHeight = 200; + static const double _cardMargin = 1; + static const double _paddingAll = 16; + static const double _spacingBetween = 16; + + final FocusNode _searchFocusNode = FocusNode(); + late final TextEditingController _searchController; + + bool get _isExpanded => _searchFocusNode.hasFocus; + + @override + void initState() { + super.initState(); + final provider = context.read(); + _searchController = TextEditingController(text: provider.query); + + _searchController.addListener(() { + provider.setQuery(_searchController.text); + }); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.error != null) { + return Center(child: Text('Error: ${provider.error}')); + } + + return SizedBox( + height: _isExpanded ? _expandedHeight : _collapsedHeight, + child: Card( + margin: const EdgeInsets.all(_cardMargin), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 4, + color: Theme.of(context).colorScheme.onSecondary, + child: Padding( + padding: const EdgeInsets.all(_paddingAll), + child: Column( + children: [ + RecipientSearchField( + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: (_) {}, + ), + const SizedBox(height: _spacingBetween), + Expanded( + child: _isExpanded + ? LongListAdressBookPayout( + filteredRecipients: provider.filteredRecipients, + onSelected: widget.onSelected, + ) + : ShortListAdressBookPayout( + recipients: provider.recipients, + onSelected: widget.onSelected, + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/form/details.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/form/details.dart new file mode 100644 index 0000000..52cd185 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/form/details.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/form.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentDetailsSection extends StatelessWidget { + final bool isFormVisible; + final bool isEditable; + final VoidCallback? onToggle; + final PaymentType? selectedType; + final Object? data; + + const PaymentDetailsSection({ + super.key, + required this.isFormVisible, + this.onToggle, + required this.selectedType, + required this.data, + required this.isEditable, + }); + + static const double toggleSpacing = 8.0; + static const double formVisibleSpacing = 30.0; + static const double formHiddenSpacing = 20.0; + static const Duration animationDuration = Duration(milliseconds: 200); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; + + final toggleIcon = isFormVisible ? Icons.expand_less : Icons.expand_more; + final toggleText = isFormVisible ? loc.hideDetails : loc.showDetails; + + return Column( + children: [ + if (!isEditable && onToggle != null) + TextButton.icon( + onPressed: onToggle, + icon: Icon(toggleIcon, color: theme.colorScheme.primary), + label: Text( + toggleText, + style: TextStyle(color: theme.colorScheme.primary), + ), + ), + const SizedBox(height: toggleSpacing), + AnimatedCrossFade( + duration: animationDuration, + crossFadeState: isFormVisible + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: PaymentMethodForm( + key: const ValueKey('formVisible'), + isEditable: isEditable, + selectedType: selectedType, + onChanged: (_) {}, + initialData: data, + ), + secondChild: const SizedBox.shrink(key: ValueKey('formHidden')), + ), + SizedBox(height: isFormVisible ? formVisibleSpacing : formHiddenSpacing), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/form/header.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/form/header.dart new file mode 100644 index 0000000..c895ebf --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/form/header.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart'; + + +class RecipientHeader extends StatelessWidget{ + final Recipient recipient; + + const RecipientHeader({super.key, required this.recipient}); + + final double _avatarRadius = 20; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return ListTile( + contentPadding: EdgeInsets.zero, + leading: RecipientAvatar( + isVisible: false, + name: recipient.name, + avatarUrl: recipient.avatarUrl, + avatarRadius: _avatarRadius, + nameStyle: Theme.of(context).textTheme.bodyMedium, + ), + title: Text(recipient.name, style: theme.textTheme.titleLarge), + subtitle: Text(recipient.email, style: theme.textTheme.bodyLarge), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/payout.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/payout.dart new file mode 100644 index 0000000..9523db6 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/payout.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/dashboard/payouts/single/new_recipient/type.dart'; + + +class SinglePayout extends StatelessWidget { + final void Function(PaymentType type) onGoToPayment; + + static const double _cardPadding = 30.0; + static const double _dividerPaddingVertical = 12.0; + static const double _cardBorderRadius = 12.0; + static const double _dividerThickness = 1.0; + + const SinglePayout({super.key, required this.onGoToPayment}); + + @override + Widget build(BuildContext context) { + final paymentTypes = PaymentType.values; + final dividerColor = Theme.of(context).dividerColor; + + return SizedBox( + width: double.infinity, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_cardBorderRadius), + ), + elevation: 4, + color: Theme.of(context).colorScheme.onSecondary, + child: Padding( + padding: const EdgeInsets.all(_cardPadding), + child: Column( + children: [ + for (int i = 0; i < paymentTypes.length; i++) ...[ + PaymentTypeTile( + type: paymentTypes[i], + onSelected: onGoToPayment, + ), + if (i < paymentTypes.length - 1) + Padding( + padding: const EdgeInsets.symmetric(vertical: _dividerPaddingVertical), + child: Divider(thickness: _dividerThickness, color: dividerColor), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/type.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/type.dart new file mode 100644 index 0000000..a62bc6b --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/new_recipient/type.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/icon.dart'; +import 'package:pweb/utils/payment/label.dart'; + + +class PaymentTypeTile extends StatelessWidget { + final PaymentType type; + final void Function(PaymentType type) onSelected; + + const PaymentTypeTile({ + super.key, + required this.type, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + final label = getPaymentTypeLabel(context, type); + + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => onSelected(type), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Icon(iconForPaymentType(type), size: 24), + const SizedBox(width: 12), + Text( + label, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/widget.dart new file mode 100644 index 0000000..b7c3198 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/dashboard/payouts/single/adress_book/widget.dart'; +import 'package:pweb/pages/dashboard/payouts/single/new_recipient/payout.dart'; + + +class SinglePayoutForm extends StatelessWidget { + final ValueChanged onRecipientSelected; + final void Function(PaymentType type) onGoToPayment; + + const SinglePayoutForm({ + super.key, + required this.onRecipientSelected, + required this.onGoToPayment, + }); + + static const double _spacingBetweenAddressAndForm = 20.0; + static const double _bottomSpacing = 40.0; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AdressBookPayout(onSelected: onRecipientSelected), + const SizedBox(height: _spacingBetweenAddressAndForm), + SinglePayout(onGoToPayment: onGoToPayment), + const SizedBox(height: _bottomSpacing), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/errors/error.dart b/frontend/pweb/lib/pages/errors/error.dart new file mode 100644 index 0000000..3f898d1 --- /dev/null +++ b/frontend/pweb/lib/pages/errors/error.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/widgets/vspacer.dart'; +import 'package:pweb/utils/error_handler.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ErrorPage extends StatelessWidget { + final String title; + final String errorMessage; + final String errorHint; + + const ErrorPage({ + super.key, + required this.title, + required this.errorMessage, + required this.errorHint, + }); + + @override + Widget build(BuildContext context) => Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, size: 72, color: Theme.of(context).colorScheme.error), + const VSpacer(), + Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ), + const VSpacer(multiplier: 0.5), + ListTile( + title: Text(errorMessage, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge), + subtitle: Text(errorHint, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall), + ), + const VSpacer(multiplier: 1.5), + TextButton( + onPressed: () => navigate(context, Pages.root), + child: Text(AppLocalizations.of(context)!.goToMainPage), + ), + ], + ), + ), + ); +} + +Widget exceptionToErrorPage({ + required BuildContext context, + required String title, + required String errorMessage, + required Object exception, +}) => ErrorPage( + title: title, + errorMessage: errorMessage, + errorHint: ErrorHandler.handleError(context, exception), +); diff --git a/frontend/pweb/lib/pages/errors/not_found.dart b/frontend/pweb/lib/pages/errors/not_found.dart new file mode 100644 index 0000000..875aee7 --- /dev/null +++ b/frontend/pweb/lib/pages/errors/not_found.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/errors/error.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class NotFoundPage extends StatelessWidget { + const NotFoundPage({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + body: ErrorPage( + title: AppLocalizations.of(context)!.errorPageNotFoundTitle, + errorMessage: AppLocalizations.of(context)!.errorPageNotFoundMessage, + errorHint: AppLocalizations.of(context)!.errorPageNotFoundHint, + ), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/loader.dart b/frontend/pweb/lib/pages/loader.dart new file mode 100644 index 0000000..da16ffe --- /dev/null +++ b/frontend/pweb/lib/pages/loader.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/loaders/account.dart'; +import 'package:pweb/pages/loaders/permissions.dart'; + + +class PageViewLoader extends StatelessWidget { + final Widget child; + + const PageViewLoader({super.key, required this.child}); + + @override + Widget build(BuildContext context) => AccountLoader( + child: PermissionsLoader( + child: child, + ), + ); +} + diff --git a/frontend/pweb/lib/pages/loaders/account.dart b/frontend/pweb/lib/pages/loaders/account.dart new file mode 100644 index 0000000..8c78257 --- /dev/null +++ b/frontend/pweb/lib/pages/loaders/account.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/account.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/widgets/error/snackbar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class AccountLoader extends StatelessWidget { + final Widget child; + + const AccountLoader({super.key, required this.child}); + + @override + Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { + if (provider.isLoading) return const Center(child: CircularProgressIndicator()); + if (provider.error != null) { + postNotifyUserOfErrorX( + context: context, + errorSituation: AppLocalizations.of(context)!.errorLogin, + exception: provider.error!, + ); + navigateAndReplace(context, Pages.login); + } + if ((provider.error == null) && (provider.account == null)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + provider.restore(); + }); + return const Center(child: CircularProgressIndicator()); + } + return child; + }); +} + diff --git a/frontend/pweb/lib/pages/loaders/organization.dart b/frontend/pweb/lib/pages/loaders/organization.dart new file mode 100644 index 0000000..3a3194f --- /dev/null +++ b/frontend/pweb/lib/pages/loaders/organization.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/organizations.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/widgets/error/snackbar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class OrganizationLoader extends StatelessWidget { + final Widget child; + + const OrganizationLoader({super.key, required this.child}); + + @override + Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { + if (provider.isLoading) return const Center(child: CircularProgressIndicator()); + if (provider.error != null) { + postNotifyUserOfErrorX( + context: context, + errorSituation: AppLocalizations.of(context)!.errorLogin, + exception: provider.error!, + ); + navigateAndReplace(context, Pages.login); + } + if ((provider.error == null) && (!provider.isOrganizationSet)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + provider.load(); + }); + return const Center(child: CircularProgressIndicator()); + } + return child; + }); +} diff --git a/frontend/pweb/lib/pages/loaders/permissions.dart b/frontend/pweb/lib/pages/loaders/permissions.dart new file mode 100644 index 0000000..1488048 --- /dev/null +++ b/frontend/pweb/lib/pages/loaders/permissions.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/permissions.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/widgets/error/snackbar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PermissionsLoader extends StatelessWidget { + final Widget child; + + const PermissionsLoader({super.key, required this.child}); + + @override + Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { + if (provider.isLoading) return const Center(child: CircularProgressIndicator()); + if (provider.error != null) { + postNotifyUserOfErrorX( + context: context, + errorSituation: AppLocalizations.of(context)!.errorLogin, + exception: provider.error!, + ); + navigateAndReplace(context, Pages.login); + } + if ((provider.error == null) && (provider.permissions.isEmpty)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + provider.load(); + }); + return const Center(child: CircularProgressIndicator()); + } + return child; + }); +} diff --git a/frontend/pweb/lib/pages/login/app_bar.dart b/frontend/pweb/lib/pages/login/app_bar.dart new file mode 100644 index 0000000..86c4977 --- /dev/null +++ b/frontend/pweb/lib/pages/login/app_bar.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/widgets/locale.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class LoginAppBar extends StatelessWidget implements PreferredSizeWidget { + const LoginAppBar({super.key}); + + @override + Widget build(BuildContext context) => AppBar( + automaticallyImplyLeading: false, + actions: const [ + LocaleChangerDropdown(availableLocales: AppLocalizations.supportedLocales), + ], + ); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/frontend/pweb/lib/pages/login/buttons.dart b/frontend/pweb/lib/pages/login/buttons.dart new file mode 100644 index 0000000..1e72a77 --- /dev/null +++ b/frontend/pweb/lib/pages/login/buttons.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/login/login.dart'; +import 'package:pweb/pages/login/signup.dart'; +import 'package:pweb/widgets/hspacer.dart'; + + +class ButtonsRow extends StatelessWidget { + final Future Function() login; + final VoidCallback onSignUp; + final bool isEnabled; + + const ButtonsRow({ + super.key, + required this.login, + required this.onSignUp, + required this.isEnabled, + }); + + @override + Widget build(BuildContext context) => Row( + children: [ + LoginButton(onPressed: isEnabled ? () => login() : null), + SignupButton(onPressed: onSignUp), + HSpacer(multiplier: 0.25), + ], + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart new file mode 100644 index 0000000..e221ea6 --- /dev/null +++ b/frontend/pweb/lib/pages/login/form.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/pfe/provider.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/pages/login/buttons.dart'; +import 'package:pweb/pages/login/header.dart'; +import 'package:pweb/widgets/constrained_form.dart'; +import 'package:pweb/widgets/password/hint/short.dart'; +import 'package:pweb/widgets/password/password.dart'; +import 'package:pweb/widgets/username.dart'; +import 'package:pweb/widgets/vspacer.dart'; +import 'package:pweb/widgets/error/snackbar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class LoginForm extends StatefulWidget { + const LoginForm({super.key}); + + @override + State createState() => _LoginFormState(); +} + +class _LoginFormState extends State { + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final _formKey = GlobalKey(); + + // ValueNotifiers for validation state + final ValueNotifier _isUsernameAcceptable = ValueNotifier(false); + final ValueNotifier _isPasswordAcceptable = ValueNotifier(false); + + Future _login(BuildContext context, VoidCallback onLogin, void Function(Object e) onError) async { + final pfeProvider = Provider.of(context, listen: false); + + try { + // final account = await pfeProvider.login( + // email: _usernameController.text, + // password: _passwordController.text, + // ); + onLogin(); + return 'ok'; + } catch (e) { + onError(pfeProvider.error == null ? e : pfeProvider.error!); + } + return null; + } + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + _isUsernameAcceptable.dispose(); + _isPasswordAcceptable.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Align( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 400, maxHeight: 300), + child: Card( + child: ConstrainedForm( + formKey: _formKey, + children: [ + const LoginHeader(), + const VSpacer(multiplier: 1.5), + UsernameField( + controller: _usernameController, + onValid: (isValid) => _isUsernameAcceptable.value = isValid, + ), + VSpacer(), + defaulRulesPasswordField( + context, + controller: _passwordController, + validationRuleBuilder: (rules, value) => shortValidation(context, rules, value), + onValid: (isValid) => _isPasswordAcceptable.value = isValid, + ), + VSpacer(multiplier: 2.0), + ValueListenableBuilder( + valueListenable: _isUsernameAcceptable, + builder: (context, isUsernameValid, child) => ValueListenableBuilder( + valueListenable: _isPasswordAcceptable, + builder: (context, isPasswordValid, child) => ButtonsRow( + onSignUp: () => navigate(context, Pages.signup), + login: () => _login( + context, + () => navigateAndReplace(context, Pages.sfactor), + (e) => postNotifyUserOfErrorX( + context: context, + errorSituation: AppLocalizations.of(context)!.errorLogin, + exception: e, + ), + ), + isEnabled: isUsernameValid && isPasswordValid, + ), + ), + ), + ], + ), + ))); + } diff --git a/frontend/pweb/lib/pages/login/header.dart b/frontend/pweb/lib/pages/login/header.dart new file mode 100644 index 0000000..a875ab5 --- /dev/null +++ b/frontend/pweb/lib/pages/login/header.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:pweb/config/constants.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/widgets/hspacer.dart'; +import 'package:pweb/widgets/logo.dart'; + + +class LoginHeader extends StatelessWidget { + const LoginHeader({super.key}); + + @override + Widget build(BuildContext context) => Row( + children: [ + const ServiceLogo(size: 36), + const HSpacer(multiplier: 0.75), + Text( + '${AppConfig.appName} ${AppLocalizations.of(context)!.login}', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ], + ); +} diff --git a/frontend/pweb/lib/pages/login/login.dart b/frontend/pweb/lib/pages/login/login.dart new file mode 100644 index 0000000..ba7c4e8 --- /dev/null +++ b/frontend/pweb/lib/pages/login/login.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/account.dart'; + +import 'package:pweb/widgets/vspacer.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class LoginButton extends StatelessWidget { + final VoidCallback? onPressed; + + const LoginButton({ + super.key, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) => Consumer(builder: (context, provider, _) => ElevatedButton( + onPressed: provider.isLoading ? null : onPressed, + child: + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (provider.isLoading) + ...[ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(), + ), + VSpacer(multiplier: 0.25), + ], + Text(AppLocalizations.of(context)!.login), + ], + ), + )); +} diff --git a/frontend/pweb/lib/pages/login/page.dart b/frontend/pweb/lib/pages/login/page.dart new file mode 100644 index 0000000..177f148 --- /dev/null +++ b/frontend/pweb/lib/pages/login/page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/login/app_bar.dart'; +import 'package:pweb/pages/login/form.dart'; +import 'package:pweb/pages/with_footer.dart'; + + +class LoginPage extends StatelessWidget { + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) => PageWithFooter( + appBar: const LoginAppBar(), + child: LoginForm(), + ); +} diff --git a/frontend/pweb/lib/pages/login/signup.dart b/frontend/pweb/lib/pages/login/signup.dart new file mode 100644 index 0000000..f7e36e8 --- /dev/null +++ b/frontend/pweb/lib/pages/login/signup.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SignupButton extends StatelessWidget { + final VoidCallback? onPressed; + + const SignupButton({ + super.key, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: onPressed, + child: Text(AppLocalizations.of(context)!.signup), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/add/card.dart b/frontend/pweb/lib/pages/payment_methods/add/card.dart new file mode 100644 index 0000000..a59ee5c --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/add/card.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_multi_formatter/flutter_multi_formatter.dart'; + +import 'package:pshared/models/payment/methods/card.dart'; + +import 'package:pweb/utils/text_field_styles.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class CardFormMinimal extends StatefulWidget { + final void Function(CardPaymentMethod) onChanged; + final CardPaymentMethod? initialData; + final bool isEditable; + + const CardFormMinimal({ + super.key, + required this.onChanged, + this.initialData, + required this.isEditable, + }); + + @override + State createState() => _CardFormMinimalState(); +} + +class _CardFormMinimalState extends State { + final _formKey = GlobalKey(); + late TextEditingController _panController; + late TextEditingController _firstNameController; + late TextEditingController _lastNameController; + + @override + void initState() { + super.initState(); + _panController = TextEditingController(text: widget.initialData?.pan ?? ''); + _firstNameController = TextEditingController(text: widget.initialData?.firstName ?? ''); + _lastNameController = TextEditingController(text: widget.initialData?.lastName ?? ''); + + WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); + } + + void _emitIfValid() { + if (_formKey.currentState?.validate() ?? false) { + widget.onChanged( + CardPaymentMethod( + pan: _panController.text.replaceAll(' ', ''), + firstName: _firstNameController.text, + lastName: _lastNameController.text, + ), + ); + } + } + + @override + void didUpdateWidget(covariant CardFormMinimal oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialData == null && oldWidget.initialData != null) { + _panController.clear(); + _firstNameController.clear(); + _lastNameController.clear(); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Form( + key: _formKey, + onChanged: _emitIfValid, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + readOnly: !widget.isEditable, + controller: _panController, + decoration: getInputDecoration(context, l10n.cardNumber, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + keyboardType: TextInputType.number, + inputFormatters: [CreditCardNumberInputFormatter()], + validator: (v) => (v == null || v.replaceAll(' ', '').length < 12) ? l10n.enterCardNumber : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _firstNameController, + decoration: getInputDecoration(context, l10n.firstName, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (v) => (v == null || v.isEmpty) ? l10n.enterFirstName : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _lastNameController, + decoration: getInputDecoration(context, l10n.lastName, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (v) => (v == null || v.isEmpty) ? l10n.enterLastName : null, + ), + ], + ), + ); + } + + @override + void dispose() { + _panController.dispose(); + _firstNameController.dispose(); + _lastNameController.dispose(); + super.dispose(); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/add/iban.dart b/frontend/pweb/lib/pages/payment_methods/add/iban.dart new file mode 100644 index 0000000..cf76227 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/add/iban.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/iban.dart'; + +import 'package:pweb/utils/text_field_styles.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class IbanForm extends StatefulWidget { + final void Function(IbanPaymentMethod) onChanged; + final IbanPaymentMethod? initialData; + final bool isEditable; + + const IbanForm({ + super.key, + required this.onChanged, + this.initialData, + required this.isEditable, + }); + + @override + State createState() => _IbanFormState(); +} + +class _IbanFormState extends State { + final _formKey = GlobalKey(); + late TextEditingController _ibanController; + late TextEditingController _accountHolderController; + late TextEditingController _bicController; + late TextEditingController _bankNameController; + + @override + void initState() { + super.initState(); + _ibanController = TextEditingController(text: widget.initialData?.iban ?? ''); + _accountHolderController = TextEditingController(text: widget.initialData?.accountHolder ?? ''); + _bicController = TextEditingController(text: widget.initialData?.bic ?? ''); + _bankNameController = TextEditingController(text: widget.initialData?.bankName ?? ''); + + WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); + } + + void _emitIfValid() { + if (_formKey.currentState?.validate() ?? false) { + widget.onChanged( + IbanPaymentMethod( + iban: _ibanController.text, + accountHolder: _accountHolderController.text, + bic: _bicController.text, + bankName: _bankNameController.text, + ), + ); + } + } + + @override + void didUpdateWidget(covariant IbanForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialData == null && oldWidget.initialData != null) { + _ibanController.clear(); + _accountHolderController.clear(); + _bicController.clear(); + _bankNameController.clear(); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Form( + key: _formKey, + onChanged: _emitIfValid, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + readOnly: !widget.isEditable, + controller: _ibanController, + decoration: getInputDecoration(context, l10n.iban, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterIban : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _accountHolderController, + decoration: getInputDecoration(context, l10n.accountHolder, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterAccountHolder : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _bicController, + decoration: getInputDecoration(context, l10n.bic, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterBic : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _bankNameController, + decoration: getInputDecoration(context, l10n.bankName, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterBankName : null, + ), + ], + ), + ); + } + + @override + void dispose() { + _ibanController.dispose(); + _accountHolderController.dispose(); + _bicController.dispose(); + _bankNameController.dispose(); + super.dispose(); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart new file mode 100644 index 0000000..9411619 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/utils/payment/label.dart'; + + +class PaymentMethodTypeSelector extends StatelessWidget { + final PaymentType? value; + final ValueChanged onChanged; + + const PaymentMethodTypeSelector({ + super.key, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return DropdownButtonFormField( + value: value, + decoration: InputDecoration(labelText: l10n.paymentType), + items: PaymentType.values.map((type) { + final label = getPaymentTypeLabel(context, type); + return DropdownMenuItem(value: type, child: Text(label)); + }).toList(), + onChanged: onChanged, + validator: (val) => val == null ? l10n.selectPaymentType : null, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart b/frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart new file mode 100644 index 0000000..a6c944d --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/add/russian_bank.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/russian_bank.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/utils/text_field_styles.dart'; + + +class RussianBankForm extends StatefulWidget { + final void Function(RussianBankAccountPaymentMethod) onChanged; + final RussianBankAccountPaymentMethod? initialData; + final bool isEditable; + + const RussianBankForm({ + super.key, + required this.onChanged, + this.initialData, + required this.isEditable, + }); + + @override + State createState() => _RussianBankFormState(); +} + +class _RussianBankFormState extends State { + final _formKey = GlobalKey(); + + late final TextEditingController _recipientNameController; + late final TextEditingController _innController; + late final TextEditingController _kppController; + late final TextEditingController _bankNameController; + late final TextEditingController _bikController; + late final TextEditingController _accountNumberController; + late final TextEditingController _correspondentAccountController; + + @override + void initState() { + super.initState(); + _recipientNameController = TextEditingController(text: widget.initialData?.recipientName ?? ''); + _innController = TextEditingController(text: widget.initialData?.inn ?? ''); + _kppController = TextEditingController(text: widget.initialData?.kpp ?? ''); + _bankNameController = TextEditingController(text: widget.initialData?.bankName ?? ''); + _bikController = TextEditingController(text: widget.initialData?.bik ?? ''); + _accountNumberController = TextEditingController(text: widget.initialData?.accountNumber ?? ''); + _correspondentAccountController = TextEditingController(text: widget.initialData?.correspondentAccount ?? ''); + + WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); + } + + void _emitIfValid() { + if (_formKey.currentState?.validate() ?? false) { + widget.onChanged( + RussianBankAccountPaymentMethod( + recipientName: _recipientNameController.text, + inn: _innController.text, + kpp: _kppController.text, + bankName: _bankNameController.text, + bik: _bikController.text, + accountNumber: _accountNumberController.text, + correspondentAccount: _correspondentAccountController.text, + ), + ); + } + } + + @override + void didUpdateWidget(covariant RussianBankForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialData == null && oldWidget.initialData != null) { + _recipientNameController.clear(); + _innController.clear(); + _kppController.clear(); + _bankNameController.clear(); + _bikController.clear(); + _accountNumberController.clear(); + _correspondentAccountController.clear(); + } + } + + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Form( + key: _formKey, + onChanged: _emitIfValid, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + readOnly: !widget.isEditable, + controller: _recipientNameController, + decoration: getInputDecoration(context, l10n.recipientName, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterRecipientName : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _innController, + decoration: getInputDecoration(context, l10n.inn, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterInn : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _kppController, + decoration: getInputDecoration(context, l10n.kpp, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterKpp : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _bankNameController, + decoration: getInputDecoration(context, l10n.bankName, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterBankName : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _bikController, + decoration: getInputDecoration(context, l10n.bik, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterBik : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _accountNumberController, + decoration: getInputDecoration(context, l10n.accountNumber, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterAccountNumber : null, + ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _correspondentAccountController, + decoration: getInputDecoration(context, l10n.correspondentAccount, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + validator: (val) => (val == null || val.isEmpty) ? l10n.enterCorrespondentAccount : null, + ), + ], + ), + ); + } + + @override + void dispose() { + _recipientNameController.dispose(); + _innController.dispose(); + _kppController.dispose(); + _bankNameController.dispose(); + _bikController.dispose(); + _accountNumberController.dispose(); + _correspondentAccountController.dispose(); + super.dispose(); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/add/wallet.dart b/frontend/pweb/lib/pages/payment_methods/add/wallet.dart new file mode 100644 index 0000000..28d136e --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/add/wallet.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/wallet.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/utils/text_field_styles.dart'; + + +class WalletForm extends StatefulWidget { + final void Function(WalletPaymentMethod) onChanged; + final WalletPaymentMethod? initialData; + final bool isEditable; + + const WalletForm({ + super.key, + required this.onChanged, + this.initialData, + required this.isEditable, + }); + + @override + State createState() => _WalletFormState(); +} + +class _WalletFormState extends State { + late TextEditingController _walletIdController; + + @override + void initState() { + super.initState(); + _walletIdController = TextEditingController(text: widget.initialData?.walletId); + } + + void _emit() { + if (_walletIdController.text.isNotEmpty) { + widget.onChanged(WalletPaymentMethod(walletId: _walletIdController.text)); + } else { + } + } + + @override + void didUpdateWidget(covariant WalletForm oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialData == null && oldWidget.initialData != null) { + _walletIdController.clear(); + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return TextFormField( + readOnly: !widget.isEditable, + controller: _walletIdController, + decoration: getInputDecoration(context, l10n.walletId, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + onChanged: (_) => _emit(), + validator: (val) => (val?.isEmpty ?? true) ? l10n.enterWalletId : null, + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/add/widget.dart b/frontend/pweb/lib/pages/payment_methods/add/widget.dart new file mode 100644 index 0000000..a1f3e90 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/add/widget.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/add/method_selector.dart'; +import 'package:pweb/pages/payment_methods/form.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class AddPaymentMethodDialog extends StatefulWidget { + const AddPaymentMethodDialog({super.key}); + + @override + State createState() => _AddPaymentMethodDialogState(); +} + +class _AddPaymentMethodDialogState extends State { + final _formKey = GlobalKey(); + PaymentType? _selectedType; + + // Holds current result from the selected form + Object? _currentMethod; + + void _submit() { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + + if (_currentMethod case final Object method) { + Navigator.of(context).pop(method); + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return AlertDialog( + title: Text(l10n.addPaymentMethod), + content: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PaymentMethodTypeSelector( + value: _selectedType, + onChanged: (val) => setState(() { + _selectedType = val; + _currentMethod = null; + }), + ), + const SizedBox(height: 16), + if (_selectedType != null) + PaymentMethodForm( + selectedType: _selectedType, + onChanged: (val) => _currentMethod = val, + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.cancel), + ), + ElevatedButton( + onPressed: _submit, + child: Text(l10n.add), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart b/frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart new file mode 100644 index 0000000..290ab18 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/delete_confirmation.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + +Future showDeleteConfirmationDialog(BuildContext context) async { + final l10n = AppLocalizations.of(context)!; + return await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text(l10n.delete), + content: Text(l10n.deletePaymentConfirmation), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(l10n.cancel), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(l10n.delete), + ), + ], + ), + ) ?? false; +} diff --git a/frontend/pweb/lib/pages/payment_methods/form.dart b/frontend/pweb/lib/pages/payment_methods/form.dart new file mode 100644 index 0000000..0010913 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/form.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/card.dart'; +import 'package:pshared/models/payment/methods/iban.dart'; +import 'package:pshared/models/payment/methods/russian_bank.dart'; +import 'package:pshared/models/payment/methods/wallet.dart'; +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/add/card.dart'; +import 'package:pweb/pages/payment_methods/add/iban.dart'; +import 'package:pweb/pages/payment_methods/add/russian_bank.dart'; +import 'package:pweb/pages/payment_methods/add/wallet.dart'; + + +class PaymentMethodForm extends StatelessWidget { + final PaymentType? selectedType; + final ValueChanged onChanged; + final Object? initialData; + final bool isEditable; + + const PaymentMethodForm({ + super.key, + required this.selectedType, + required this.onChanged, + this.initialData, + this.isEditable = true, + }); + + @override + Widget build(BuildContext context) { + return switch (selectedType) { + PaymentType.card => CardFormMinimal( + onChanged: onChanged, + initialData: initialData as CardPaymentMethod?, + isEditable: isEditable, + ), + PaymentType.iban => IbanForm( + onChanged: onChanged, + initialData: initialData as IbanPaymentMethod?, + isEditable: isEditable, + ), + PaymentType.wallet => WalletForm( + onChanged: onChanged, + initialData: initialData as WalletPaymentMethod?, + isEditable: isEditable, + ), + PaymentType.bankAccount => RussianBankForm( + onChanged: onChanged, + initialData: initialData as RussianBankAccountPaymentMethod?, + isEditable: isEditable, + ), + _ => const SizedBox.shrink(), + }; + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/icon.dart b/frontend/pweb/lib/pages/payment_methods/icon.dart new file mode 100644 index 0000000..faab1be --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/icon.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + + +IconData iconForPaymentType(PaymentType type) { + switch (type) { + case PaymentType.bankAccount: + return Icons.account_balance; + case PaymentType.iban: + return Icons.language; + case PaymentType.wallet: + return Icons.account_balance_wallet; + case PaymentType.card: + return Icons.credit_card; + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/page.dart b/frontend/pweb/lib/pages/payment_methods/page.dart new file mode 100644 index 0000000..5f0f861 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/page.dart @@ -0,0 +1,232 @@ +import 'package:amplitude_flutter/amplitude.dart'; +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/dashboard/payouts/payment_form.dart'; +import 'package:pweb/pages/dashboard/payouts/single/form/details.dart'; +import 'package:pweb/pages/dashboard/payouts/single/form/header.dart'; +import 'package:pweb/providers/payment_methods.dart'; +import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/services/amplitude.dart'; +import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/utils/payment/dropdown.dart'; +import 'package:pweb/utils/payment/selector_type.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/widgets/sidebar/destinations.dart'; + + +//TODO: decide whether to make AppDimensions universal for the whole app or leave it as it is - unique for this page alone + + +class PaymentPage extends StatefulWidget { + final PaymentType? type; + final ValueChanged? onBack; + + const PaymentPage({super.key, this.type, this.onBack}); + + @override + State createState() => _PaymentPageState(); +} + +class _PaymentPageState extends State { + late Map _availableTypes; + late PaymentType _selectedType; + bool _isFormVisible = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final recipientProvider = context.watch(); + final methodsProvider = context.watch(); + final recipient = recipientProvider.selectedRecipient; + + // Initialize available types based on whether we have a recipient + if (recipient != null) { + // We have a recipient - use their payment methods + _availableTypes = { + if (recipient.card != null) PaymentType.card: recipient.card!, + if (recipient.iban != null) PaymentType.iban: recipient.iban!, + if (recipient.wallet != null) PaymentType.wallet: recipient.wallet!, + if (recipient.bank != null) PaymentType.bankAccount: recipient.bank!, + }; + + // Set selected type if it's available, otherwise use first available type + if (_availableTypes.containsKey(_selectedType)) { + // Keep current selection if valid + } else if (_availableTypes.isNotEmpty) { + _selectedType = _availableTypes.keys.first; + } else { + // Fallback if recipient has no payment methods + _selectedType = PaymentType.bankAccount; + } + } else { + // No recipient - we're creating a new payment from scratch + _availableTypes = {}; + _selectedType = widget.type ?? PaymentType.bankAccount; + _isFormVisible = true; // Always show form when creating new payment + } + + // Load payment methods if not already loaded + if (methodsProvider.methods.isEmpty && !methodsProvider.isLoading) { + WidgetsBinding.instance.addPostFrameCallback((_) { + methodsProvider.loadMethods(); + }); + } + } + + @override + void initState() { + super.initState(); + // Initial values + _availableTypes = {}; + _selectedType = widget.type ?? PaymentType.bankAccount; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final dimensions = AppDimensions(); + final recipientProvider = context.watch(); + final methodsProvider = context.watch(); + final recipient = recipientProvider.selectedRecipient; + + // Show loading state for payment methods + if (methodsProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + // Show error state for payment methods + if (methodsProvider.error != null) { + return Center( + child: Text('Error: ${methodsProvider.error}'), + ); + } + + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), + child: Material( + elevation: dimensions.elevationSmall, + borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), + color: theme.colorScheme.onSecondary, + child: Padding( + padding: EdgeInsets.all(dimensions.paddingLarge), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Back button + Align( + alignment: Alignment.topLeft, + child: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + widget.onBack?.call(recipient); + }, + ), + ), + SizedBox(height: dimensions.paddingSmall), + + // Header + Row( + children: [ + Icon( + Icons.send_rounded, + color: theme.colorScheme.primary, + size: dimensions.iconSizeLarge + ), + SizedBox(width: dimensions.spacingSmall), + Text( + AppLocalizations.of(context)!.sendTo, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold + ), + ), + ], + ), + SizedBox(height: dimensions.paddingXXLarge), + + // Payment method dropdown (user's payment methods) + PaymentMethodDropdown( + methods: methodsProvider.methods, + initialValue: methodsProvider.selectedMethod, + onChanged: (method) { + methodsProvider.selectMethod(method); + }, + ), + SizedBox(height: dimensions.paddingXLarge), + + // Recipient section (only show if we have a recipient) + if (recipient != null) ...[ + RecipientHeader(recipient: recipient), + SizedBox(height: dimensions.paddingMedium), + + // Payment type selector (recipient's payment methods) + if (_availableTypes.isNotEmpty) + PaymentTypeSelector( + availableTypes: _availableTypes, + selectedType: _selectedType, + onSelected: (type) => setState(() => _selectedType = type), + ), + SizedBox(height: dimensions.paddingMedium), + ], + + // Payment details section + PaymentDetailsSection( + isFormVisible: recipient == null || _isFormVisible, + onToggle: recipient != null + ? () => setState(() => _isFormVisible = !_isFormVisible) + : null, // No toggle when creating new payment + selectedType: _selectedType, + data: _availableTypes[_selectedType], + isEditable: recipient == null, + ), + + const PaymentFormWidget(), + + SizedBox(height: dimensions.paddingXXXLarge), + + Center( + child: SizedBox( + width: dimensions.buttonWidth, + height: dimensions.buttonHeight, + child: InkWell( + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + onTap: () => + // TODO: Handle Payment logic + AmplitudeService.pageOpened(PayoutDestination.payment), //TODO: replace with payment event + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + ), + child: Center( + child: Text( + AppLocalizations.of(context)!.send, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSecondary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ), + SizedBox(height: dimensions.paddingLarge), + ], + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/title.dart b/frontend/pweb/lib/pages/payment_methods/title.dart new file mode 100644 index 0000000..54136fd --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/title.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/payment_methods/icon.dart'; +import 'package:pshared/models/payment/methods/type.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + +class PaymentMethodTile extends StatelessWidget { + const PaymentMethodTile({ + super.key, + required this.method, + required this.index, + required this.makeMain, + required this.toggleEnabled, + required this.edit, + required this.delete, + }); + + final PaymentMethod method; + final int index; + final VoidCallback makeMain; + final ValueChanged toggleEnabled; + final VoidCallback edit; + final VoidCallback delete; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + return Opacity( + opacity: method.isEnabled ? 1 : 0.5, + child: Card( + margin: const EdgeInsets.symmetric(vertical: 4), + elevation: 0, + child: ListTile( + key: ValueKey(method.id), + leading: Icon(iconForPaymentType(method.type)), + onTap: makeMain, + title: Row( + children: [ + Expanded(child: Text(method.label)), + Text( + method.details, + style: theme.textTheme.bodySmall, + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildMakeMainButton(context), + _buildEnabledSwitch(), + _buildPopupMenu(l10n), + ], + ), + ), + ), + ); + } + + Widget _buildMakeMainButton(BuildContext context) { + final theme = Theme.of(context); + return IconButton( + tooltip: 'Make main', + icon: Icon( + method.isMain ? Icons.star : Icons.star_outline, + color: method.isMain ? theme.colorScheme.primary : null, + ), + onPressed: makeMain, + ); + } + + Widget _buildEnabledSwitch() { + return Switch.adaptive( + value: method.isEnabled, + onChanged: toggleEnabled, + ); + } + + Widget _buildPopupMenu(AppLocalizations l10n) { + return PopupMenuButton( + tooltip: l10n.moreActions, + onSelected: (value) { + switch (value) { + case 'edit': + edit(); + break; + case 'delete': + delete(); + break; + } + }, + itemBuilder: (_) => [ + PopupMenuItem(value: 'edit', child: Text(l10n.edit)), + PopupMenuItem(value: 'delete', child: Text(l10n.delete)), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/methods/advanced.dart b/frontend/pweb/lib/pages/payment_page/methods/advanced.dart new file mode 100644 index 0000000..cf9256f --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/methods/advanced.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentConfigAdvanced extends StatelessWidget { + const PaymentConfigAdvanced({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return ExpansionTile( + title: Text(l10n.advanced), + tilePadding: const EdgeInsets.symmetric(horizontal: 16), + childrenPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + children: [Text(l10n.fallbackExplanation)], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/methods/controller.dart b/frontend/pweb/lib/pages/payment_page/methods/controller.dart new file mode 100644 index 0000000..e0496f4 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/methods/controller.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/methods/type.dart'; + +import 'package:pweb/providers/payment_methods.dart'; +import 'package:pweb/pages/payment_methods/add/widget.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentConfigController { + final BuildContext context; + + PaymentConfigController(this.context); + + void loadMethods() { + context.read().loadMethods(); + } + + Future addMethod() async { + await showDialog( + context: context, + builder: (_) => const AddPaymentMethodDialog(), + ); + loadMethods(); + } + + Future editMethod(PaymentMethod method) async { + // TODO: implement edit functionality + } + + Future deleteMethod(PaymentMethod method) async { + final l10n = AppLocalizations.of(context)!; + final confirmed = await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text(l10n.delete), + content: Text(l10n.deletePaymentConfirmation), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(l10n.cancel), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + child: Text(l10n.delete), + ), + ], + ), + ); + + if (confirmed == true) { + context.read().deleteMethod(method); + } + } + + void toggleEnabled(PaymentMethod method, bool value) { + context.read().toggleEnabled(method, value); + } + + void makeMain(PaymentMethod method) { + context.read().makeMain(method); + } + + void reorder(int oldIndex, int newIndex) { + context.read().reorderMethods(oldIndex, newIndex); + } +} diff --git a/frontend/pweb/lib/pages/payment_page/methods/header.dart b/frontend/pweb/lib/pages/payment_page/methods/header.dart new file mode 100644 index 0000000..0c935ed --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/methods/header.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentConfigHeader extends StatelessWidget { + final VoidCallback onAdd; + const PaymentConfigHeader({super.key, required this.onAdd}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + return Column( + children: [ + Text( + l10n.paymentConfigTitle, + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text(l10n.paymentConfigSubtitle, textAlign: TextAlign.center), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + icon: const Icon(Icons.add), + label: Text(l10n.addPaymentMethod), + onPressed: onAdd, + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_page/methods/list.dart b/frontend/pweb/lib/pages/payment_page/methods/list.dart new file mode 100644 index 0000000..3c98471 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/methods/list.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/pages/payment_methods/title.dart'; +import 'package:pweb/pages/payment_page/methods/controller.dart'; +import 'package:pweb/providers/payment_methods.dart'; + + +class PaymentConfigList extends StatelessWidget { + final PaymentConfigController controller; + const PaymentConfigList({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: provider.methods.length, + onReorder: controller.reorder, + itemBuilder: (context, index) { + final method = provider.methods[index]; + return ReorderableDragStartListener( + key: Key(method.id), + index: index, + child: PaymentMethodTile( + method: method, + index: index, + makeMain: () => controller.makeMain(method), + toggleEnabled: (v) => controller.toggleEnabled(method, v), + edit: () => controller.editMethod(method), + delete: () => controller.deleteMethod(method), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/methods/widget.dart b/frontend/pweb/lib/pages/payment_page/methods/widget.dart new file mode 100644 index 0000000..5ae0623 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/methods/widget.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/payment_page/methods/advanced.dart'; +import 'package:pweb/pages/payment_page/methods/controller.dart'; +import 'package:pweb/pages/payment_page/methods/header.dart'; +import 'package:pweb/pages/payment_page/methods/list.dart'; + + +class MethodsWidget extends StatefulWidget { + const MethodsWidget({super.key}); + + @override + State createState() => _MethodsWidgetState(); +} + +class _MethodsWidgetState extends State { + late final PaymentConfigController controller; + + @override + void initState() { + super.initState(); + controller = PaymentConfigController(context); + controller.loadMethods(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: theme.cardTheme.elevation ?? 4, + color: theme.colorScheme.onSecondary, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PaymentConfigHeader(onAdd: controller.addMethod), + const SizedBox(height: 12), + PaymentConfigList(controller: controller), + const SizedBox(height: 12), + const PaymentConfigAdvanced(), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/page.dart b/frontend/pweb/lib/pages/payment_page/page.dart new file mode 100644 index 0000000..2898ff7 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/page.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; +import 'package:pweb/models/wallet.dart'; + +import 'package:pweb/pages/payment_page/methods/widget.dart'; +import 'package:pweb/pages/payment_page/wallet/wigets.dart'; +import 'package:pweb/providers/payment_methods.dart'; + + +class PaymentConfigPage extends StatelessWidget { + final Function(Wallet) onWalletTap; + + const PaymentConfigPage({super.key, required this.onWalletTap}); + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (provider.error != null) { + return Center(child: Text('Error: ${provider.error}')); + } + + return Column( + children: [ + MethodsWidget(), + Expanded( + child: WalletWidgets(onWalletTap: onWalletTap), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/card.dart b/frontend/pweb/lib/pages/payment_page/wallet/card.dart new file mode 100644 index 0000000..6341477 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/card.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; +import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/utils/currency.dart'; + + +class WalletCard extends StatelessWidget { + final Wallet wallet; + final VoidCallback onTap; + + const WalletCard({super.key, required this.wallet, required this.onTap}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: theme.cardTheme.elevation ?? 4, + color: theme.colorScheme.onSecondary, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Container( + padding: const EdgeInsets.only(left: 50, top: 16, bottom: 16), + child: Row( + spacing: 3, + children: [ + CircleAvatar( + radius: 24, + child: Icon(iconForCurrencyType(wallet.currency), size: 28), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BalanceAmount( + wallet: wallet, + onToggleVisibility: () { + context.read().toggleVisibility(wallet.id); + }, + ), + Text( + wallet.name, + style: theme.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/buttons.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/buttons.dart new file mode 100644 index 0000000..a9aad41 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/buttons.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:pweb/pages/payment_page/wallet/edit/buttons/send.dart'; +import 'package:pweb/pages/payment_page/wallet/edit/buttons/top_up.dart'; +import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class ButtonsWalletWidget extends StatelessWidget { + const ButtonsWalletWidget({super.key}); + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + final wallet = provider.wallets?.first; + + if (wallet == null) return const SizedBox.shrink(); + + final dimensions = AppDimensions(); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceBright, + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.primary.withAlpha(50), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: SendPayoutButton(), + ), + VerticalDivider( + color: Theme.of(context).colorScheme.primary, + thickness: 1, + width: 10, + ), + Expanded( + child: TopUpButton(), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/save.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/save.dart new file mode 100644 index 0000000..8d64fea --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/save.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class SaveWalletButton extends StatelessWidget { + final Wallet wallet; + final TextEditingController nameController; + final TextEditingController balanceController; + final VoidCallback onSave; // Changed to VoidCallback + + const SaveWalletButton({ + super.key, + required this.wallet, + required this.nameController, + required this.balanceController, + required this.onSave, // Now matches _saveWallet signature + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final dimensions = AppDimensions(); + + return Center( + child: SizedBox( + width: dimensions.buttonWidth, + height: dimensions.buttonHeight, + child: InkWell( + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + onTap: onSave, // Directly use onSave now + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + ), + child: Center( + child: Text( + 'Save', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSecondary, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart new file mode 100644 index 0000000..daae32f --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/send.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/providers/wallets.dart'; + + +class SendPayoutButton extends StatelessWidget { + + const SendPayoutButton({ + super.key, + }); + + + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + shadowColor: null, + elevation: 0, + ), + onPressed: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Add functionality')), + ), + child: Text('Send Payout'), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/top_up.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/top_up.dart new file mode 100644 index 0000000..898fdb9 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/edit/buttons/top_up.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + + +class TopUpButton extends StatelessWidget{ + const TopUpButton({super.key}); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + shadowColor: null, + elevation: 0, + ), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Add functionality')), + ); + }, + child: Text('Top Up Balance'), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart new file mode 100644 index 0000000..9462336 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/edit/fields.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:provider/provider.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; + +import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/utils/currency.dart'; + + +class WalletEditFields extends StatelessWidget { + + const WalletEditFields({super.key}); + + @override + Widget build(BuildContext context) { + final wallet = context.watch().wallets?.first; + + if (wallet == null) { + return const SizedBox.shrink(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + BalanceAmount( + wallet: wallet, + onToggleVisibility: () { + context.read().toggleVisibility(wallet.id); + }, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(wallet.walletUserID, style: Theme.of(context).textTheme.bodyLarge), + IconButton( + icon: Icon(Icons.copy), + iconSize: 18, + onPressed: () => Clipboard.setData(ClipboardData(text: wallet.walletUserID)), + ), + ], + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart new file mode 100644 index 0000000..113aa18 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/edit/header.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/utils/currency.dart'; +import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/providers/wallets.dart'; + + +// class WalletEditHeader extends StatefulWidget { +// const WalletEditHeader({super.key}); + +// @override +// State createState() => _WalletEditHeaderState(); +// } + +// class _WalletEditHeaderState extends State { +// bool _isEditing = false; +// late TextEditingController _controller; + +// @override +// void initState() { +// super.initState(); +// _controller = TextEditingController(); +// } + +// @override +// void dispose() { +// _controller.dispose(); +// super.dispose(); +// } + +// @override +// Widget build(BuildContext context) { +// final provider = context.watch(); +// final currentWallet = provider.getWalletById(provider.wallets!.id); + + +// if (wallet == null) { +// return const SizedBox.shrink(); +// } + +// final theme = Theme.of(context); +// final dimensions = AppDimensions(); + +// if (!_isEditing) { +// _controller.text = wallet.name; +// } + +// return Row( +// spacing: 8, +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Icon( +// iconForCurrencyType(wallet.currency), +// color: theme.colorScheme.primary, +// size: dimensions.iconSizeLarge, +// ), + +// Expanded( +// child: !_isEditing +// ? Row( +// children: [ +// Expanded( +// child: Text( +// wallet.name, +// style: theme.textTheme.headlineMedium!.copyWith( +// fontWeight: FontWeight.bold,), +// ), +// ), +// IconButton( +// icon: const Icon(Icons.edit), +// onPressed: () { +// setState(() { +// _isEditing = true; +// }); +// }, +// ), +// ], +// ) +// : Row( +// children: [ +// Expanded( +// child: TextFormField( +// controller: _controller, +// decoration: const InputDecoration( +// border: OutlineInputBorder(), +// isDense: true, +// hintText: 'Wallet name', +// ), +// ), +// ), +// IconButton( +// icon: const Icon(Icons.check), +// color: theme.colorScheme.primary, +// onPressed: () async { +// provider.updateName(wallet.id, _controller.text); +// await provider.updateWallet(wallet.copyWith(name: _controller.text)); +// ScaffoldMessenger.of(context).showSnackBar( +// const SnackBar(content: Text('Wallet name saved')), +// ); +// setState(() { +// _isEditing = false; +// }); +// }, +// ), +// IconButton( +// icon: const Icon(Icons.close), +// onPressed: () { +// _controller.text = wallet.name; +// setState(() { +// _isEditing = false; +// }); +// }, +// ), +// ], +// ), +// ), +// ], +// ); +// } +// } \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart b/frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart new file mode 100644 index 0000000..e50bc3d --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/edit/page.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/pages/payment_page/wallet/edit/buttons/buttons.dart'; +import 'package:pweb/pages/payment_page/wallet/edit/fields.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class WalletEditPage extends StatelessWidget { + final Wallet wallet; + final VoidCallback onBack; + + const WalletEditPage({super.key, required this.wallet, required this.onBack}); + + @override + Widget build(BuildContext context) { + final dimensions = AppDimensions(); + + return Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), + child: Material( + elevation: dimensions.elevationSmall, + color: Theme.of(context).colorScheme.onSecondary, + borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), + child: Padding( + padding: EdgeInsets.all(dimensions.paddingLarge), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: onBack, + ), + + // WalletEditHeader(), + + WalletEditFields(), + + const SizedBox(height: 24), + + ButtonsWalletWidget(), + + const SizedBox(height: 24), + ], + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_page/wallet/wigets.dart b/frontend/pweb/lib/pages/payment_page/wallet/wigets.dart new file mode 100644 index 0000000..66901e8 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_page/wallet/wigets.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; +import 'package:pweb/models/wallet.dart'; + +import 'package:pweb/pages/payment_page/wallet/card.dart'; +import 'package:pweb/providers/wallets.dart'; + + +class WalletWidgets extends StatelessWidget { + final Function(Wallet) onWalletTap; + + const WalletWidgets({super.key, required this.onWalletTap}); + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + + final wallets = provider.wallets; + + if (wallets == null) { + return const Center(child: CircularProgressIndicator()); + } + + return GridView.builder( + scrollDirection: Axis.vertical, + physics: AlwaysScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 3, + ), + itemCount: wallets.length, + itemBuilder: (context, index) { + final wallet = wallets[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: WalletCard( + wallet: wallet, + onTap: () { + onWalletTap(wallet); + }, + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/report/charts/distribution.dart b/frontend/pweb/lib/pages/report/charts/distribution.dart new file mode 100644 index 0000000..b491985 --- /dev/null +++ b/frontend/pweb/lib/pages/report/charts/distribution.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; + +import 'package:syncfusion_flutter_charts/charts.dart'; + +import 'package:pshared/models/payment/operation.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PayoutDistributionChart extends StatelessWidget { + final List operations; + const PayoutDistributionChart({super.key, required this.operations}); + + @override + Widget build(BuildContext context) { + // 1) Aggregate sums + final sums = {}; + for (var op in operations) { + final name = op.name ?? AppLocalizations.of(context)!.unknown; + sums[name] = (sums[name] ?? 0) + op.amount; + } + if (sums.isEmpty) { + return Center(child: Text(AppLocalizations.of(context)!.noPayouts)); + } + + // 2) Build chart data + final data = sums.entries + .map((e) => _ChartData(e.key, e.value)) + .toList(); + + // 3) Build a simple horizontal legend + final palette = [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.secondary, + Theme.of(context).colorScheme.tertiary ?? Colors.grey, + Theme.of(context).colorScheme.primaryContainer, + Theme.of(context).colorScheme.secondaryContainer, + ]; + final legendItems = List.generate(data.length, (i) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.circle, size: 10, color: palette[i % palette.length]), + const SizedBox(width: 4), + Text(data[i].label, style: Theme.of(context).textTheme.bodySmall), + if (i < data.length - 1) const SizedBox(width: 12), + ], + ); + }); + + return Card( + margin: const EdgeInsets.all(16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Pie takes 2/3 of the width + Expanded( + flex: 2, + child: SfCircularChart( + legend: Legend(isVisible: false), + tooltipBehavior: TooltipBehavior(enable: true), + series: >[ + PieSeries<_ChartData, String>( + dataSource: data, + xValueMapper: (d, _) => d.label, + yValueMapper: (d, _) => d.value, + dataLabelMapper: (d, _) => + '${(d.value / sums.values.fold(0, (a, b) => a + b) * 100).toStringAsFixed(1)}%', + dataLabelSettings: const DataLabelSettings( + isVisible: true, + labelPosition: ChartDataLabelPosition.inside, + ), + radius: '100%', + ) + ], + ), + ), + + const SizedBox(width: 16), + + // Legend takes 1/3 + Expanded( + flex: 1, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Column(spacing: 4.0, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: legendItems), + ), + ), + ], + ), + ), + ); + } +} + +class _ChartData { + final String label; + final double value; + _ChartData(this.label, this.value); +} diff --git a/frontend/pweb/lib/pages/report/charts/status.dart b/frontend/pweb/lib/pages/report/charts/status.dart new file mode 100644 index 0000000..27ea708 --- /dev/null +++ b/frontend/pweb/lib/pages/report/charts/status.dart @@ -0,0 +1,91 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:syncfusion_flutter_charts/charts.dart'; + +import 'package:pshared/models/payment/status.dart'; +import 'package:pshared/models/payment/operation.dart'; + + +class StatusChart extends StatelessWidget { + final List operations; + + const StatusChart({super.key, required this.operations}); + + @override + Widget build(BuildContext context) { + // 1) Compute counts + final counts = {}; + for (var op in operations) { + counts[op.status] = (counts[op.status] ?? 0) + 1; + } + final items = counts.entries + .map((e) => _ChartData(e.key, e.value.toDouble())) + .toList(); + final maxCount = items.map((e) => e.count.toInt()).fold(0, max); + + final theme = Theme.of(context); + final barColor = theme.colorScheme.secondary; + final caption = theme.textTheme.labelMedium; + + return SizedBox( + height: 200, + child: Card( + margin: const EdgeInsets.all(16), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8), + child: SfCartesianChart( + // ─── Axes ───────────────────────────────────────── + primaryXAxis: CategoryAxis( + labelStyle: caption, + majorGridLines: const MajorGridLines(width: 0), + ), + primaryYAxis: NumericAxis( + minimum: 0, + maximum: (maxCount + 1).toDouble(), + interval: 1, + labelStyle: caption, + majorGridLines: MajorGridLines( + color: theme.dividerColor.withAlpha(76), + width: 1, + dashArray: [4, 2], + ), + ), + + // ─── Enable tooltips ─────────────────────────────── + legend: Legend(isVisible: false), + tooltipBehavior: TooltipBehavior( + enable: true, + header: '', // omit series name in header + format: 'point.x : point.y', // e.g. "Init : 2" + ), + + // ─── Bar series with tooltip enabled ─────────────── + series: >[ + ColumnSeries<_ChartData, String>( + dataSource: items, + xValueMapper: (d, _) => d.status.localized(context), + yValueMapper: (d, _) => d.count, + color: barColor, + width: 0.6, + borderRadius: const BorderRadius.all(Radius.circular(4)), + enableTooltip: true, // <— turn on for this series + ), + ], + ), + ), + ), + ); + } +} + +class _ChartData { + final OperationStatus status; + final double count; + _ChartData(this.status, this.count); +} diff --git a/frontend/pweb/lib/pages/report/page.dart b/frontend/pweb/lib/pages/report/page.dart new file mode 100644 index 0000000..4904e3b --- /dev/null +++ b/frontend/pweb/lib/pages/report/page.dart @@ -0,0 +1,170 @@ +// operation_history_page.dart +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/operation.dart'; +import 'package:pshared/models/payment/status.dart'; + +import 'package:pweb/pages/report/charts/distribution.dart'; +import 'package:pweb/pages/report/charts/status.dart'; +import 'package:pweb/pages/report/table/filters.dart'; +import 'package:pweb/pages/report/table/widget.dart'; + + +class OperationHistoryPage extends StatefulWidget { + const OperationHistoryPage({super.key}); + + @override + State createState() => _OperationHistoryPageState(); +} + +class _OperationHistoryPageState extends State { + // Mock data + final List _allOps = [ + OperationItem( + status: OperationStatus.error, + fileName: 'cards_payout_sample_june.csv', + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '860163800', + cardNumber: null, + name: 'John Snow', + date: DateTime(2025, 7, 14, 19, 59, 2), + comment: 'EUR visa', + ), + OperationItem( + status: OperationStatus.processing, + fileName: 'cards_payout_sample_june.csv', + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '860163700', + cardNumber: null, + name: 'Baltasar Gelt', + date: DateTime(2025, 7, 14, 19, 59, 2), + comment: 'EUR master', + ), + OperationItem( + status: OperationStatus.error, + fileName: 'cards_payout_sample_june.csv', + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '40000000****0077', + cardNumber: '40000000****0077', + name: 'John Snow', + date: DateTime(2025, 7, 14, 19, 23, 22), + comment: 'EUR visa', + ), + OperationItem( + status: OperationStatus.success, + fileName: null, + amount: 10, + currency: 'EUR', + toAmount: 10, + toCurrency: 'EUR', + payId: '54133300****0019', + cardNumber: '54133300****0019', + name: 'Baltasar Gelt', + date: DateTime(2025, 7, 14, 19, 23, 21), + comment: 'EUR master', + ), + OperationItem( + status: OperationStatus.success, + fileName: null, + amount: 130, + currency: 'EUR', + toAmount: 130, + toCurrency: 'EUR', + payId: '54134300****0019', + cardNumber: '54153300****0019', + name: 'Ivan Brokov', + date: DateTime(2025, 7, 15, 19, 23, 21), + comment: 'EUR master 2', + ), + ]; + DateTimeRange? _range; + final Set _statuses = {}; + late List _filtered; + + @override + void initState() { + super.initState(); + _filtered = List.from(_allOps); + } + + void _applyFilter() { + setState(() { + _filtered = _allOps.where((op) { + final okStatus = _statuses.isEmpty || _statuses.contains(op.status.localized(context)); + final okRange = _range == null || + (op.date.isAfter(_range!.start.subtract(const Duration(seconds: 1))) && + op.date.isBefore(_range!.end.add(const Duration(seconds: 1)))); + return okStatus && okRange; + }).toList(); + }); + } + + Future _pickRange() async { + final now = DateTime.now(); + final initial = _range ?? + DateTimeRange( + start: now.subtract(const Duration(days: 30)), + end: now, + ); + final picked = await showDateRangePicker( + context: context, + firstDate: DateTime(2000), + lastDate: now.add(const Duration(days: 1)), + initialDateRange: initial, + ); + if (picked != null) { + setState(() => _range = picked); + } + } + + void _toggleStatus(String status) { + setState(() { + if (_statuses.contains(status)) _statuses.remove(status); + else _statuses.add(status); + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 16, + children: [ + SizedBox( + height: 200, // same height for both + child: Row( + spacing: 16, + children: [ + Expanded(child: StatusChart(operations: _allOps)), + Expanded(child: PayoutDistributionChart(operations: _allOps)), + ], + ), + ), + OperationFilters( + selectedRange: _range, + selectedStatuses: _statuses, + onPickRange: _pickRange, + onToggleStatus: _toggleStatus, + onApply: _applyFilter, + ), + OperationsTable( + operations: _filtered, + showFileNameColumn: + _allOps.any((op) => op.fileName != null), + ), + ], + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/report/table/badge.dart b/frontend/pweb/lib/pages/report/table/badge.dart new file mode 100644 index 0000000..960d905 --- /dev/null +++ b/frontend/pweb/lib/pages/report/table/badge.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:badges/badges.dart' as badges; + +import 'package:pshared/models/payment/status.dart'; + + +class OperationStatusBadge extends StatelessWidget { + final OperationStatus status; + + const OperationStatusBadge({super.key, required this.status}); + + Color _badgeColor(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + switch (status) { + case OperationStatus.processing: + return scheme.primary; + case OperationStatus.success: + return scheme.secondary; + case OperationStatus.error: + return scheme.error; + } + } + + Color _textColor(Color background) { + // computeLuminance returns 0 for black, 1 for white + return background.computeLuminance() > 0.5 ? Colors.black : Colors.white; + } + + @override + Widget build(BuildContext context) { + final label = status.localized(context); + final bg = _badgeColor(context); + final fg = _textColor(bg); + + return badges.Badge( + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.square, + badgeColor: bg, + borderRadius: BorderRadius.circular(12), // fully rounded + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2 // tighter padding + ), + ), + badgeContent: Text( + label.toUpperCase(), // or keep sentence case + style: TextStyle( + color: fg, + fontSize: 11, // smaller text + fontWeight: FontWeight.w500, // medium weight + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/report/table/filters.dart b/frontend/pweb/lib/pages/report/table/filters.dart new file mode 100644 index 0000000..4c4583d --- /dev/null +++ b/frontend/pweb/lib/pages/report/table/filters.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:badges/badges.dart' as badges; // Make sure to add badges package in pubspec.yaml +import 'package:pshared/models/payment/status.dart'; +import 'package:pshared/utils/localization.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + +class OperationFilters extends StatelessWidget { + final DateTimeRange? selectedRange; + final Set selectedStatuses; + final VoidCallback onPickRange; + final VoidCallback onApply; + final ValueChanged onToggleStatus; + + const OperationFilters({ + super.key, + required this.selectedRange, + required this.selectedStatuses, + required this.onPickRange, + required this.onApply, + required this.onToggleStatus, + }); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Card( + margin: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.filters, + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 12), + GestureDetector( + onTap: onPickRange, + child: Row( + children: [ + Icon(Icons.date_range_outlined, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Expanded( + child: Text( + selectedRange == null + ? l10n.selectPeriod + : '${dateToLocalFormat(context, selectedRange!.start)} – ${dateToLocalFormat(context, selectedRange!.end)}', + style: TextStyle( + color: selectedRange == null + ? Colors.grey + : Colors.black87, + ), + ), + ), + Icon(Icons.keyboard_arrow_down, color: Colors.grey), + ], + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 8, + children: [ + OperationStatus.success.localized(context), + OperationStatus.processing.localized(context), + OperationStatus.error.localized(context), + ].map((status) { + final isSelected = selectedStatuses.contains(status); + return GestureDetector( + onTap: () => onToggleStatus(status), + child: badges.Badge( + badgeAnimation: badges.BadgeAnimation.fade(), + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.square, + badgeColor: isSelected + ? Theme.of(context).primaryColor + : Colors.grey.shade300, + borderRadius: BorderRadius.circular(8), + ), + badgeContent: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Text( + l10n.status(status), + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontSize: 14, + ), + ), + ), + ), + ); + }).toList(), + ), + const SizedBox(height: 24), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: onApply, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text(l10n.apply), + ), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/report/table/row.dart b/frontend/pweb/lib/pages/report/table/row.dart new file mode 100644 index 0000000..7398610 --- /dev/null +++ b/frontend/pweb/lib/pages/report/table/row.dart @@ -0,0 +1,23 @@ +// operation_row.dart +import 'package:flutter/material.dart'; +import 'package:pshared/models/payment/operation.dart'; +import 'package:pweb/pages/report/table/badge.dart'; + +class OperationRow { + static DataRow build(OperationItem op, BuildContext context) { + return DataRow(cells: [ + DataCell(OperationStatusBadge(status: op.status)), + DataCell(Text(op.fileName ?? '')), + DataCell(Text('${op.amount.toStringAsFixed(2)} ${op.currency}')), + DataCell(Text('${op.toAmount.toStringAsFixed(2)} ${op.toCurrency}')), + DataCell(Text(op.payId)), + DataCell(Text(op.cardNumber ?? '-')), + DataCell(Text(op.name)), + DataCell(Text( + '${TimeOfDay.fromDateTime(op.date).format(context)}\n' + '${op.date.toLocal().toIso8601String().split("T").first}', + )), + DataCell(Text(op.comment)), + ]); + } +} diff --git a/frontend/pweb/lib/pages/report/table/widget.dart b/frontend/pweb/lib/pages/report/table/widget.dart new file mode 100644 index 0000000..fb64d90 --- /dev/null +++ b/frontend/pweb/lib/pages/report/table/widget.dart @@ -0,0 +1,63 @@ +// operations_table.dart +import 'package:flutter/material.dart'; +import 'package:pshared/models/payment/operation.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/pages/report/table/row.dart'; + +class OperationsTable extends StatelessWidget { + final List operations; + final bool showFileNameColumn; + + const OperationsTable({ + super.key, + required this.operations, + required this.showFileNameColumn, + }); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return Expanded( + child: SingleChildScrollView( + child: DataTable( + columnSpacing: 24, + headingTextStyle: const TextStyle( + fontWeight: FontWeight.bold, + ), + columns: [ + DataColumn(label: Text(l10n.statusColumn)), + DataColumn(label: Text(l10n.fileNameColumn)), + DataColumn(label: Text(l10n.amountColumn)), + DataColumn(label: Text(l10n.toAmountColumn)), + DataColumn(label: Text(l10n.payIdColumn)), + DataColumn(label: Text(l10n.cardNumberColumn)), + DataColumn(label: Text(l10n.nameColumn)), + DataColumn(label: Text(l10n.dateColumn)), + DataColumn(label: Text(l10n.commentColumn)), + ], + rows: List.generate( + operations.length, + (index) { + final op = operations[index]; + // Alternate row colors + final color = WidgetStateProperty.resolveWith((states) { + return index.isEven + ? Theme.of(context).colorScheme.surfaceContainerHighest + : null; + }); + + // Use the DataRow built by OperationRow and extract its cells + final row = OperationRow.build(op, context); + return DataRow.byIndex( + index: index, + color: color, + cells: row.cells, + ); + }, + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/settings/profile/account/avatar.dart b/frontend/pweb/lib/pages/settings/profile/account/avatar.dart new file mode 100644 index 0000000..0f80127 --- /dev/null +++ b/frontend/pweb/lib/pages/settings/profile/account/avatar.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +//import 'package:provider/provider.dart'; + +import 'package:image_picker/image_picker.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class AvatarTile extends StatefulWidget { + final String? avatarUrl; + final String title; + final String description; + final String errorText; + + const AvatarTile({ + super.key, + required this.avatarUrl, + required this.title, + required this.description, + required this.errorText, + }); + + @override + State createState() => _AvatarTileState(); +} + +class _AvatarTileState extends State { + static const double _avatarSize = 96.0; + static const double _iconSize = 32.0; + static const double _titleSpacing = 4.0; + static const String _placeholderAsset = 'assets/images/avatar_placeholder.png'; + + bool _isHovering = false; + + Future _pickImage() async { + final picker = ImagePicker(); + final file = await picker.pickImage(source: ImageSource.gallery); + if (file != null) { + debugPrint('Selected new avatar: ${file.path}'); + } + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; + final safeUrl = + widget.avatarUrl?.trim().isNotEmpty == true ? widget.avatarUrl : null; + final theme = Theme.of(context); + + return Column( + children: [ + MouseRegion( + onEnter: (_) => setState(() => _isHovering = true), + onExit: (_) => setState(() => _isHovering = false), + child: GestureDetector( + onTap: _pickImage, + child: Stack( + alignment: Alignment.center, + children: [ + ClipOval( + child: safeUrl != null + ? Image.network( + safeUrl, + width: _avatarSize, + height: _avatarSize, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildPlaceholder(), + ) + : _buildPlaceholder(), + ), + if (_isHovering) + ClipOval( + child: Container( + width: _avatarSize, + height: _avatarSize, + color: theme.colorScheme.primary.withAlpha(90), + child: Icon( + Icons.camera_alt, + color: theme.colorScheme.onSecondary, + size: _iconSize, + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: _titleSpacing), + Text( + loc.avatarHint, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSecondary, + ), + ), + ], + ); + } + + Widget _buildPlaceholder() { + return Image.asset( + _placeholderAsset, + width: _avatarSize, + height: _avatarSize, + fit: BoxFit.cover, + ); + } +} diff --git a/frontend/pweb/lib/pages/settings/profile/account/locale.dart b/frontend/pweb/lib/pages/settings/profile/account/locale.dart new file mode 100644 index 0000000..31d5982 --- /dev/null +++ b/frontend/pweb/lib/pages/settings/profile/account/locale.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/locale.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/services/amplitude.dart'; + + +class LocalePicker extends StatelessWidget { + final String title; + + const LocalePicker({ + super.key, + required this.title, + }); + + static const double _pickerWidth = 300; + static const double _iconSize = 20; + static const double _gapMedium = 6; + static const double _gapLarge = 8; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; + + return Consumer( + builder: (context, localeProvider, _) { + final currentLocale = localeProvider.locale; + final options = AppLocalizations.supportedLocales; + + return SizedBox( + width: _pickerWidth, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.language_outlined, color: theme.colorScheme.primary, size: _iconSize), + const SizedBox(width: _gapMedium), + Text(title, style: theme.textTheme.bodyMedium), + ], + ), + const SizedBox(height: _gapLarge), + DropdownButtonFormField( + initialValue: currentLocale, + items: options + .map( + (locale) => DropdownMenuItem( + value: locale, + child: Text(_localizedLocaleName(locale, loc)), + ), + ) + .toList(), + onChanged: (locale) { + if (locale != null) { + localeProvider.setLocale(locale); + AmplitudeService.localeChanged(locale); + } + }, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + ], + ), + ); + }, + ); + } + + String _localizedLocaleName(Locale locale, AppLocalizations loc) { + switch (locale.languageCode) { + case 'en': + return 'English'; + case 'ru': + return 'Русский'; + case 'de': + return 'Deutsch'; + default: + return locale.toString(); + } + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/settings/profile/account/name.dart b/frontend/pweb/lib/pages/settings/profile/account/name.dart new file mode 100644 index 0000000..cf231e0 --- /dev/null +++ b/frontend/pweb/lib/pages/settings/profile/account/name.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; + + +class AccountName extends StatefulWidget { + final String name; + final String title; + final String hintText; + final String errorText; + + const AccountName({ + super.key, + required this.name, + required this.title, + required this.hintText, + required this.errorText, + }); + + @override + State createState() => _AccountNameState(); +} + +class _AccountNameState extends State { + static const double _inputWidth = 200; + static const double _spacing = 8; + static const double _errorSpacing = 4; + static const double _borderWidth = 2; + + late final TextEditingController _controller; + bool _isEditing = false; + late String _originalName; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.name); + _originalName = widget.name; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _startEditing() => setState(() => _isEditing = true); + + void _cancelEditing() { + setState(() { + _controller.text = _originalName; + _isEditing = false; + }); + } + + void _saveEditing() { + setState(() { + _originalName = _controller.text; + _isEditing = false; + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isEditing) + SizedBox( + width: _inputWidth, + child: TextFormField( + controller: _controller, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + autofocus: true, + decoration: InputDecoration( + hintText: widget.hintText, + isDense: true, + border: UnderlineInputBorder( + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: _borderWidth, + ), + ), + ), + ), + ) + else + Text( + _originalName, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: _spacing), + if (_isEditing) ...[ + IconButton( + icon: Icon(Icons.check, color: theme.colorScheme.primary), + onPressed: _saveEditing, + ), + IconButton( + icon: Icon(Icons.close, color: theme.colorScheme.error), + onPressed: _cancelEditing, + ), + ] else + IconButton( + icon: Icon(Icons.edit, color: theme.colorScheme.primary), + onPressed: _startEditing, + ), + ], + ), + const SizedBox(height: _errorSpacing), + if (widget.errorText.isEmpty) + Text( + widget.errorText, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.error, + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/settings/profile/page.dart b/frontend/pweb/lib/pages/settings/profile/page.dart new file mode 100644 index 0000000..ce850f4 --- /dev/null +++ b/frontend/pweb/lib/pages/settings/profile/page.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/settings/profile/account/avatar.dart'; +import 'package:pweb/pages/settings/profile/account/locale.dart'; +import 'package:pweb/pages/settings/profile/account/name.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ProfileSettingsPage extends StatelessWidget { + const ProfileSettingsPage({super.key}); + + static const _cardPadding = EdgeInsets.symmetric(vertical: 32, horizontal: 16); + static const _cardRadius = 16.0; + static const _itemSpacing = 12.0; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + return Material( + elevation: 4, + borderRadius: BorderRadius.circular(_cardRadius), + clipBehavior: Clip.antiAlias, + color: theme.colorScheme.onSecondary, + child: Padding( + padding: _cardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: _itemSpacing, + children: [ + AvatarTile( + avatarUrl: 'https://avatars.githubusercontent.com/u/65651201', + title: loc.avatar, + description: loc.avatarHint, + errorText: loc.avatarUpdateError, + ), + AccountName( + name: 'User Name', + title: loc.accountName, + hintText: loc.accountNameHint, + errorText: loc.accountNameUpdateError, + ), + LocalePicker( + title: loc.language, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/settings/widgets/base.dart b/frontend/pweb/lib/pages/settings/widgets/base.dart new file mode 100644 index 0000000..31a6a96 --- /dev/null +++ b/frontend/pweb/lib/pages/settings/widgets/base.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/utils/error/snackbar.dart'; + +enum _EditState { view, edit, saving } + +/// Базовый класс, управляющий состояниями (view/edit/saving), +/// показом snackbar ошибок и успешного сохранения. +abstract class BaseEditTile extends AbstractSettingsTile { + const BaseEditTile({ + super.key, + required this.icon, + required this.title, + required this.valueGetter, + required this.valueSetter, + required this.errorSituation, + }); + + final IconData icon; + final String title; + final ValueGetter valueGetter; + final Future Function(T) valueSetter; + final String errorSituation; + + /// Рисует в режиме просмотра (read-only). + Widget buildView(BuildContext context, T? value); + + /// Рисует UI редактора. + /// Если [useDialogEditor]==true, его обернут в диалог. + Widget buildEditor( + BuildContext context, + T? initial, + void Function(T) onSave, + VoidCallback onCancel, + bool isSaving, + ); + + /// true → показывать редактор в диалоге, false → inline под заголовком. + bool get useDialogEditor => false; + + @override + Widget build(BuildContext context) => _BaseEditTileBody(delegate: this); +} + +class _BaseEditTileBody extends StatefulWidget { + const _BaseEditTileBody({required this.delegate}); + final BaseEditTile delegate; + @override + State<_BaseEditTileBody> createState() => _BaseEditTileBodyState(); +} + +class _BaseEditTileBodyState extends State<_BaseEditTileBody> { + _EditState _state = _EditState.view; + bool get _isSaving => _state == _EditState.saving; + + Future _performSave(T newValue) async { + final current = widget.delegate.valueGetter(); + if (newValue == current) { + setState(() => _state = _EditState.view); + return; + } + setState(() => _state = _EditState.saving); + final sms = ScaffoldMessenger.of(context); + final locs = AppLocalizations.of(context)!; + try { + await widget.delegate.valueSetter(newValue); + sms.showSnackBar(SnackBar( + content: Text(locs.settingsSuccessfullyUpdated), + duration: const Duration(milliseconds: 1200), + )); + } catch (e) { + notifyUserOfErrorX( + scaffoldMessenger: sms, + errorSituation: widget.delegate.errorSituation, + appLocalizations: locs, + exception: e, + ); + } finally { + if (mounted) setState(() => _state = _EditState.view); + } + } + + Future _openDialogEditor() async { + final initial = widget.delegate.valueGetter(); + final T? result = await showDialog( + context: context, + barrierDismissible: true, + builder: (ctx) { + return Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), + child: Padding( + padding: const EdgeInsets.all(16), + child: widget.delegate.buildEditor( + ctx, + initial, + (v) => Navigator.of(ctx).pop(v), + () => Navigator.of(ctx).pop(), + _isSaving, + ), + ), + ); + }, + ); + if (result != null) await _performSave(result); + } + + @override + Widget build(BuildContext context) { + final delegate = widget.delegate; + final current = delegate.valueGetter(); + + // Диалоговый режим + if (delegate.useDialogEditor) { + return SettingsTile.navigation( + leading: Icon(delegate.icon), + title: Text(delegate.title), + value: delegate.buildView(context, current), + onPressed: (_) => _openDialogEditor(), + ); + } + + // Inline-режим (под заголовком будет редактор прямо в tile) + return SettingsTile.navigation( + leading: Icon(delegate.icon), + title: Text(delegate.title), + value: _state == _EditState.view + ? delegate.buildView(context, current) + : delegate.buildEditor( + context, + current, + _performSave, + () => setState(() => _state = _EditState.view), + _isSaving, + ), + onPressed: (_) { + if (_state == _EditState.view) setState(() => _state = _EditState.edit); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/settings/widgets/image.dart b/frontend/pweb/lib/pages/settings/widgets/image.dart new file mode 100644 index 0000000..ed076bb --- /dev/null +++ b/frontend/pweb/lib/pages/settings/widgets/image.dart @@ -0,0 +1,112 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; + +import 'package:image_picker/image_picker.dart'; + +import 'package:pweb/utils/error/snackbar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ImageTile extends AbstractSettingsTile { + final String? imageUrl; + final double? maxWidth; + final double? maxHeight; + final String? imageUpdateError; + final Future Function(XFile?) onUpdate; + final String? title; + final String? description; + final Widget? imagePreview; + final double previewWidth; + final double previewHeight; + + const ImageTile({ + super.key, + required this.imageUrl, + this.maxWidth, + this.maxHeight, + this.imageUpdateError, + required this.onUpdate, + this.title, + this.description, + this.imagePreview, + this.previewHeight = 40.0, + this.previewWidth = 40.0, + }); + + Future _pickImage(BuildContext context) async { + final picker = ImagePicker(); + final locs = AppLocalizations.of(context)!; + final sm = ScaffoldMessenger.of(context); + final picked = await picker.pickImage( + source: ImageSource.gallery, + maxWidth: maxWidth, + maxHeight: maxHeight, + ); + if (picked == null) return; + + try { + await onUpdate(picked); + if (imageUrl != null) { + CachedNetworkImage.evictFromCache(imageUrl!); + } + } catch (e) { + notifyUserOfErrorX( + scaffoldMessenger: sm, + errorSituation: imageUpdateError ?? locs.settingsImageUpdateError, + exception: e, + appLocalizations: locs, + ); + } + } + + @override + Widget build(BuildContext context) => SettingsTile.navigation( + leading: imagePreview ?? + ClipRRect( + borderRadius: BorderRadius.circular(0.1 * (previewWidth < previewHeight ? previewWidth : previewHeight)), + child: imageUrl != null + ? CachedNetworkImage( + imageUrl: imageUrl!, + width: previewWidth, + height: previewHeight, + fit: BoxFit.cover, + progressIndicatorBuilder: (ctx, url, downloadProgress) { + // compute 10% of the smaller image dimension, but no more than 40px + final baseSize = min(previewWidth, previewHeight) * 0.1; + final indicatorSize = baseSize.clamp(0.0, 40.0); + + return Center( + child: SizedBox( + width: indicatorSize, + height: indicatorSize, + child: CircularProgressIndicator( + value: downloadProgress.progress, // from 0.0 to 1.0 + strokeWidth: max(indicatorSize * 0.1, 2.0), // 10% of size, but at least 2px so it’s visible + ), + ), + ); + }, + errorWidget: (ctx, url, err) => const Icon(Icons.error), + ) + : Container( + width: previewWidth, + height: previewHeight, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + child: Icon( + Icons.image_not_supported, + size: previewWidth * 0.6, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + title: Text(title ?? AppLocalizations.of(context)!.settingsImageTitle), + description: Text(description ?? AppLocalizations.of(context)!.settingsImageHint), + onPressed: (_) => _pickImage(context), + ); +} diff --git a/frontend/pweb/lib/pages/settings/widgets/pick.dart b/frontend/pweb/lib/pages/settings/widgets/pick.dart new file mode 100644 index 0000000..7bf7933 --- /dev/null +++ b/frontend/pweb/lib/pages/settings/widgets/pick.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/pages/settings/widgets/base.dart'; + + +const _kSearchThreshold = 12; + +class SelectValueTile extends BaseEditTile { + final double? maxEditorHeight; + final double? maxEditorWidth; + + const SelectValueTile({ + super.key, + required super.icon, + required super.title, + required super.valueGetter, + required super.valueSetter, + required super.errorSituation, + required this.options, + required this.labelBuilder, + this.filterOptions, + this.maxEditorHeight, + this.maxEditorWidth, + }); + + final List options; + final String Function(T) labelBuilder; + final List Function(String)? filterOptions; + + @override + bool get useDialogEditor => true; + + @override + Widget buildView(BuildContext context, T? value) { + return Text( + value == null ? AppLocalizations.of(context)!.notSet : labelBuilder(value), + ); + } + + @override + Widget buildEditor( + BuildContext context, + T? initial, + void Function(T) onSave, + VoidCallback onCancel, + bool isSaving, + ) { + // local state for the current search query + String searchText = ''; + + return StatefulBuilder( + builder: (context, setState) { + // decide which list to show + final displayedOptions = (options.length > _kSearchThreshold && filterOptions != null && searchText.isNotEmpty) + ? filterOptions!(searchText) + : options; + + final content = Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (options.length > _kSearchThreshold && filterOptions != null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), + child: TextField( + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.search, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + isDense: true, + ), + onChanged: (value) { + // update the local searchText and rebuild the list + setState(() { + searchText = value; + }); + }, + ), + ), + Flexible( + child: ListView( + shrinkWrap: true, + children: displayedOptions.map((o) => RadioListTile( + value: o, + groupValue: initial, + title: Text(labelBuilder(o)), + onChanged: isSaving ? null : (v) { if (v != null) onSave(v); }, + )).toList(), + ), + ), + const Divider(), + TextButton( + onPressed: onCancel, + child: Text(AppLocalizations.of(context)!.cancel), + ), + ], + ); + + // if the caller passed a max size, enforce it: + if (maxEditorHeight != null || maxEditorWidth != null) { + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: maxEditorHeight ?? double.infinity, + maxWidth: maxEditorWidth ?? double.infinity, + ), + child: content, + ); + } + + return content; + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/settings/widgets/text.dart b/frontend/pweb/lib/pages/settings/widgets/text.dart new file mode 100644 index 0000000..a297ca9 --- /dev/null +++ b/frontend/pweb/lib/pages/settings/widgets/text.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/settings/widgets/base.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class TextEditTile extends BaseEditTile { + const TextEditTile({ + super.key, + required super.icon, + required super.title, + required super.valueGetter, + required super.valueSetter, + required super.errorSituation, + required this.hintText, + }); + + final String hintText; + + @override + Widget buildView(BuildContext context, String? value) { + final locs = AppLocalizations.of(context)!; + final display = (value ?? '').isEmpty ? locs.notSet : value!; + return Text( + display, + semanticsLabel: (value ?? '').isEmpty ? locs.notSet : '$title: $display', + ); + } + + @override + Widget buildEditor( + BuildContext context, + String? initial, + void Function(String) onSave, + VoidCallback onCancel, + bool isSaving, + ) { + final controller = TextEditingController(text: initial ?? ''); + return Row( + children: [ + Expanded( + child: TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + hintText: hintText, + isDense: true, + contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6), + ), + onSubmitted: (_) => onSave(controller.text.trim()), + ), + ), + const SizedBox(width: 8.0), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: isSaving + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Row( + key: const ValueKey('actions'), + children: [ + Tooltip( + message: AppLocalizations.of(context)!.ok, + child: IconButton( + icon: const Icon(Icons.check), + onPressed: () => onSave(controller.text.trim()), + visualDensity: VisualDensity.compact, + ), + ), + Tooltip( + message: AppLocalizations.of(context)!.cancel, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: onCancel, + visualDensity: VisualDensity.compact, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/signup/buttons.dart b/frontend/pweb/lib/pages/signup/buttons.dart new file mode 100644 index 0000000..6776687 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/buttons.dart @@ -0,0 +1,31 @@ + +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SignUpButtonsRow extends StatelessWidget { + final VoidCallback onLogin; + final VoidCallback signUp; + final bool isEnabled; + + const SignUpButtonsRow({ + super.key, + required this.onLogin, + required this.signUp, + required this.isEnabled, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: isEnabled ? signUp : null, + child: Text(AppLocalizations.of(context)!.signup), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/signup/form/buttons.dart b/frontend/pweb/lib/pages/signup/form/buttons.dart new file mode 100644 index 0000000..754c77f --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/buttons.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + + +class SignUpBackButton extends StatelessWidget { + const SignUpBackButton({super.key}); + + @override + Widget build(BuildContext context) => Row( + children: [ + IconButton( + onPressed: Navigator.of(context).pop, + icon: const Icon(Icons.arrow_back), + ), + ], + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/signup/form/content.dart b/frontend/pweb/lib/pages/signup/form/content.dart new file mode 100644 index 0000000..93cce12 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/content.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/signup/buttons.dart'; +import 'package:pweb/pages/signup/form/controllers.dart'; +import 'package:pweb/pages/signup/form/feilds.dart'; +import 'package:pweb/widgets/constrained_form.dart'; + + +class SignUpFormContent extends StatelessWidget { + final GlobalKey formKey; + final SignUpFormControllers controllers; + final bool autoValidateMode; + final VoidCallback onSignUp; + final VoidCallback onLogin; + + const SignUpFormContent({ + required this.formKey, + required this.controllers, + required this.autoValidateMode, + required this.onSignUp, + required this.onLogin, + super.key, + }); + + @override + Widget build(BuildContext context) => Align( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 700), + child: Card( + child: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + IconButton( + onPressed: Navigator.of(context).pop, + icon: Icon(Icons.arrow_back), + ), + ], + ), + ConstrainedForm( + formKey: formKey, + autovalidateMode: autoValidateMode + ? AutovalidateMode.onUserInteraction + : AutovalidateMode.disabled, + children: [ + SignUpFormFields(controllers: controllers), + SignUpButtonsRow( + onLogin: onLogin, + signUp: onSignUp, + isEnabled: true, + ), + ], + ), + ], + ), + ), + ), + ), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/signup/form/controllers.dart b/frontend/pweb/lib/pages/signup/form/controllers.dart new file mode 100644 index 0000000..da78c9c --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/controllers.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + + +class SignUpFormControllers { + final TextEditingController companyName = TextEditingController(); + final TextEditingController description = TextEditingController(); + final TextEditingController firstName = TextEditingController(); + final TextEditingController lastName = TextEditingController(); + final TextEditingController email = TextEditingController(); + final TextEditingController password = TextEditingController(); + final TextEditingController passwordConfirm = TextEditingController(); + + void dispose() { + companyName.dispose(); + description.dispose(); + firstName.dispose(); + lastName.dispose(); + email.dispose(); + password.dispose(); + passwordConfirm.dispose(); + } +} diff --git a/frontend/pweb/lib/pages/signup/form/description.dart b/frontend/pweb/lib/pages/signup/form/description.dart new file mode 100644 index 0000000..c672792 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/description.dart @@ -0,0 +1,24 @@ + +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class DescriptionField extends StatelessWidget { + final TextEditingController controller; + + const DescriptionField({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: '${AppLocalizations.of(context)!.companyDescription} (${AppLocalizations.of(context)!.optional})', + hintText: AppLocalizations.of(context)!.companyDescriptionHint, + ), + maxLines: 3, + maxLength: 500, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/signup/form/email.dart b/frontend/pweb/lib/pages/signup/form/email.dart new file mode 100644 index 0000000..a8fddb3 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/email.dart @@ -0,0 +1,32 @@ + +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + +//TODO check with /widgets/username.dart + +class EmailField extends StatelessWidget { + final TextEditingController controller; + + const EmailField({super.key, required this.controller}); + + static final _emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.username, + hintText: AppLocalizations.of(context)!.usernameHint, + ), + validator: (value) { + if (value == null || !_emailRegex.hasMatch(value)) { + return AppLocalizations.of(context)!.usernameErrorInvalid; + } + return null; + }, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/signup/form/feilds.dart b/frontend/pweb/lib/pages/signup/form/feilds.dart new file mode 100644 index 0000000..d62ce67 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/feilds.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/signup/form/controllers.dart'; +import 'package:pweb/pages/signup/form/description.dart'; +import 'package:pweb/pages/signup/form/email.dart'; +import 'package:pweb/pages/signup/header.dart'; +import 'package:pweb/widgets/password/hint/short.dart'; +import 'package:pweb/widgets/password/password.dart'; +import 'package:pweb/widgets/password/verify.dart'; +import 'package:pweb/widgets/text_field.dart'; +import 'package:pweb/widgets/vspacer.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SignUpFormFields extends StatelessWidget { + final SignUpFormControllers controllers; + + const SignUpFormFields({ + required this.controllers, + super.key, + }); + + @override + Widget build(BuildContext context) => Column( + children: [ + const SignUpHeader(), + const VSpacer(), + NotEmptyTextFormField( + controller: controllers.companyName, + labelText: AppLocalizations.of(context)!.companyName, + readOnly: false, + error: AppLocalizations.of(context)!.companynameRequired, + ), + const VSpacer(), + DescriptionField( + controller: controllers.description, + ), + const VSpacer(), + NotEmptyTextFormField( + controller: controllers.firstName, + labelText: AppLocalizations.of(context)!.lastName, + readOnly: false, + error: AppLocalizations.of(context)!.enterLastName, + ), + const VSpacer(), + NotEmptyTextFormField( + controller: controllers.lastName, + labelText: AppLocalizations.of(context)!.firstName, + readOnly: false, + error: AppLocalizations.of(context)!.enterFirstName, + ), + const VSpacer(), + EmailField(controller: controllers.email), + const VSpacer(), + defaulRulesPasswordField( + context, + controller: controllers.password, + validationRuleBuilder: (rules, value) => + shortValidation(context, rules, value), + ), + const VSpacer(multiplier: 2.0), + VerifyPasswordField( + controller: controllers.passwordConfirm, + externalPasswordController: controllers.password, + ), + const VSpacer(multiplier: 2.0), + ], + ); +} diff --git a/frontend/pweb/lib/pages/signup/form/form.dart b/frontend/pweb/lib/pages/signup/form/form.dart new file mode 100644 index 0000000..16627e9 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/form.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/signup/form/state.dart'; + + +class SignUpForm extends StatefulWidget { + const SignUpForm({super.key}); + + @override + State createState() => SignUpFormState(); +} diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart new file mode 100644 index 0000000..a25ec84 --- /dev/null +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/pfe/provider.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/pages/signup/form/content.dart'; +import 'package:pweb/pages/signup/form/controllers.dart'; +import 'package:pweb/pages/signup/form/form.dart'; +import 'package:pweb/widgets/error/snackbar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SignUpFormState extends State { + late final SignUpFormControllers controllers; + final _formKey = GlobalKey(); + bool _autoValidateMode = false; + + @override + void initState() { + super.initState(); + controllers = SignUpFormControllers(); + } + + Future signUp( + BuildContext context, + VoidCallback onSignUp, + void Function(Object e) onError, + ) async { + final pfeProvider = Provider.of(context, listen: false); + + setState(() { + _autoValidateMode = true; + }); + + if (!(_formKey.currentState?.validate() ?? false)) { + return null; + } + + try { + // final account = await pfeProvider.signUp( + // companyName: controllers.companyName.text.trim(), + // description: controllers.description.text.trim().isEmpty + // ? null + // : controllers.description.text.trim(), + // firstName: controllers.firstName.text.trim(), + // lastName: controllers.lastName.text.trim(), + // email: controllers.email.text.trim(), + // password: controllers.password.text, + // ); + onSignUp(); + return 'ok'; + } catch (e) { + onError(pfeProvider.error ?? e); + } + return null; + } + + void handleSignUp() => signUp( + context, + () { + context.goNamed( + Pages.sfactor.name, + queryParameters: {'from': 'signup'}, + ); + }, + (e) => postNotifyUserOfErrorX( + context: context, + errorSituation: AppLocalizations.of(context)!.errorSignUp, + exception: e, + ), + ); + + void handleLogin() => navigate(context, Pages.login); + + @override + void dispose() { + controllers.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => SignUpFormContent( + formKey: _formKey, + controllers: controllers, + autoValidateMode: _autoValidateMode, + onSignUp: handleSignUp, + onLogin: handleLogin, + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/signup/header.dart b/frontend/pweb/lib/pages/signup/header.dart new file mode 100644 index 0000000..2c0198b --- /dev/null +++ b/frontend/pweb/lib/pages/signup/header.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:pweb/config/constants.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/widgets/hspacer.dart'; +import 'package:pweb/widgets/logo.dart'; + + +class SignUpHeader extends StatelessWidget { + const SignUpHeader({super.key}); + + @override + Widget build(BuildContext context) => Row( + children: [ + const ServiceLogo(size: 36), + const HSpacer(multiplier: 0.75), + Text( + '${AppConfig.appName} ${AppLocalizations.of(context)!.signup}', + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ], + ); +} diff --git a/frontend/pweb/lib/pages/signup/page.dart b/frontend/pweb/lib/pages/signup/page.dart new file mode 100644 index 0000000..752b78c --- /dev/null +++ b/frontend/pweb/lib/pages/signup/page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; + +import 'package:pweb/pages/login/app_bar.dart'; +import 'package:pweb/pages/signup/form/form.dart'; +import 'package:pweb/pages/with_footer.dart'; + + +class SignUpPage extends StatelessWidget { + const SignUpPage({super.key}); + + @override + Widget build(BuildContext context) => PageWithFooter( + appBar: const LoginAppBar(), + child: SignUpForm(), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/with_footer.dart b/frontend/pweb/lib/pages/with_footer.dart new file mode 100644 index 0000000..f3f2ae8 --- /dev/null +++ b/frontend/pweb/lib/pages/with_footer.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/footer/widget.dart'; + + +class PageWithFooter extends StatelessWidget { + final PreferredSizeWidget? appBar; + final Widget child; + + const PageWithFooter({super.key, required this.child, this.appBar}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: appBar, + body: Column( + children: [ + Expanded(child: child), + FooterWidget(), + ], + ), + ); +} diff --git a/frontend/pweb/lib/providers/balance.dart b/frontend/pweb/lib/providers/balance.dart new file mode 100644 index 0000000..95c9c76 --- /dev/null +++ b/frontend/pweb/lib/providers/balance.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/services/balance.dart'; + + +class BalanceProvider with ChangeNotifier { + final BalanceService _service; + + BalanceProvider(this._service); + + double? _balance; + String? _walletName; + String? _walletId; + bool _isHidden = true; + + double? get balance => _balance; + String? get walletName => _walletName; + String? get walletId => _walletId; + bool get isHidden => _isHidden; + + Future loadData() async { + _balance = await _service.getBalance(); + _walletName = await _service.getWalletName(); + _walletId = await _service.getWalletId(); + notifyListeners(); + } + + void toggleVisibility() { + _isHidden = !_isHidden; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/providers/carousel.dart b/frontend/pweb/lib/providers/carousel.dart new file mode 100644 index 0000000..8a7bfcd --- /dev/null +++ b/frontend/pweb/lib/providers/carousel.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + + +class CarouselIndexProvider extends ChangeNotifier { + int _currentIndex = 0; + + int get currentIndex => _currentIndex; + + void updateIndex(int index) { + if (_currentIndex != index) { + _currentIndex = index; + notifyListeners(); + } + } +} diff --git a/frontend/pweb/lib/providers/mock_payment.dart b/frontend/pweb/lib/providers/mock_payment.dart new file mode 100644 index 0000000..6c8ed24 --- /dev/null +++ b/frontend/pweb/lib/providers/mock_payment.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + + +class MockPaymentProvider with ChangeNotifier { + double _amount = 10.0; + bool _payerCoversFee = true; + + double get amount => _amount; + bool get payerCoversFee => _payerCoversFee; + + double get fee => _amount * 0.05; + double get total => payerCoversFee ? (_amount + fee) : _amount; + double get recipientGets => payerCoversFee ? _amount : (_amount - fee); + + void setAmount(double value) { + _amount = value; + notifyListeners(); + } + + void setPayerCoversFee(bool value) { + _payerCoversFee = value; + notifyListeners(); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/providers/page_selector.dart b/frontend/pweb/lib/providers/page_selector.dart new file mode 100644 index 0000000..b1b1874 --- /dev/null +++ b/frontend/pweb/lib/providers/page_selector.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/widgets/sidebar/destinations.dart'; +import 'package:pweb/services/amplitude.dart'; +import 'package:pweb/providers/recipient.dart'; + + +class PageSelectorProvider extends ChangeNotifier { + PayoutDestination _selected = PayoutDestination.dashboard; + PaymentType? _type; + bool _cameFromRecipientList = false; + + final RecipientProvider? recipientProvider; + final WalletsProvider? walletsProvider; + + PayoutDestination get selected => _selected; + PaymentType? get type => _type; + + PageSelectorProvider({this.recipientProvider, this.walletsProvider}); + + void selectPage(PayoutDestination dest) { + _selected = dest; + notifyListeners(); + } + + void selectRecipient(Recipient? recipient, {bool fromList = false}) { + if (recipientProvider != null) { + recipientProvider!.selectRecipient(recipient); + _cameFromRecipientList = fromList; + _selected = PayoutDestination.payment; + notifyListeners(); + } else { + debugPrint("RecipientProvider is null — cannot select recipient"); + } + } + + void editRecipient(Recipient? recipient, {bool fromList = false}) { + if (recipientProvider != null) { + recipientProvider!.selectRecipient(recipient); + _cameFromRecipientList = fromList; + _selected = PayoutDestination.addrecipient; + notifyListeners(); + } else { + debugPrint("RecipientProvider is null — cannot select recipient"); + } + } + + void goToAddRecipient() { + if (recipientProvider != null) { + AmplitudeService.recipientAddStarted(); + recipientProvider!.selectRecipient(null); + _selected = PayoutDestination.addrecipient; + _cameFromRecipientList = false; + notifyListeners(); + } else { + debugPrint("RecipientProvider is null — cannot go to add recipient"); + } + } + + void startPaymentWithoutRecipient(PaymentType type) { + if (recipientProvider != null) { + recipientProvider!.selectRecipient(null); + } + _type = type; + _cameFromRecipientList = false; + _selected = PayoutDestination.payment; + notifyListeners(); + } + + void goBackFromPayment() { + _selected = _cameFromRecipientList + ? PayoutDestination.recipients + : PayoutDestination.dashboard; + _type = null; + notifyListeners(); + } + + void goBackFromWalletEdit() { + selectPage(PayoutDestination.methods); + } + + void selectWallet(Wallet wallet) { + if (walletsProvider != null) { + walletsProvider!.selectWallet(wallet); + _selected = PayoutDestination.editwallet; + notifyListeners(); + } else { + debugPrint("RecipientProvider is null — cannot select wallet"); + } + } + + Recipient? get selectedRecipient => recipientProvider?.selectedRecipient; + Wallet? get selectedWallet => walletsProvider?.selectedWallet; +} \ No newline at end of file diff --git a/frontend/pweb/lib/providers/payment_methods.dart b/frontend/pweb/lib/providers/payment_methods.dart new file mode 100644 index 0000000..22dc3d7 --- /dev/null +++ b/frontend/pweb/lib/providers/payment_methods.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/type.dart'; + +import 'package:pweb/services/payments/payment_methods.dart'; + + +class PaymentMethodsProvider extends ChangeNotifier { + final PaymentMethodsService service; + + List _methods = []; + PaymentMethod? _selectedMethod; + bool _isLoading = false; + String? _error; + + PaymentMethodsProvider({required this.service}); + + List get methods => _methods; + PaymentMethod? get selectedMethod => _selectedMethod; + bool get isLoading => _isLoading; + String? get error => _error; + + Future loadMethods() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _methods = await service.fetchMethods(); + _selectedMethod = _methods.firstWhere((m) => m.isMain, orElse: () => _methods.first); + } catch (e) { + _error = e.toString(); + } + + _isLoading = false; + notifyListeners(); + } + + void selectMethod(PaymentMethod method) { + _selectedMethod = method; + notifyListeners(); + } + + void deleteMethod(PaymentMethod method) { + _methods.remove(method); + notifyListeners(); + } + + void reorderMethods(int oldIndex, int newIndex) { + if (newIndex > oldIndex) newIndex--; + final item = _methods.removeAt(oldIndex); + _methods.insert(newIndex, item); + notifyListeners(); + } + + void toggleEnabled(PaymentMethod method, bool value) { + method.isEnabled = value; + notifyListeners(); + } + + void makeMain(PaymentMethod method) { + for (final m in _methods) m.isMain = false; + method.isMain = true; + selectMethod(method); + } +} diff --git a/frontend/pweb/lib/providers/recipient.dart b/frontend/pweb/lib/providers/recipient.dart new file mode 100644 index 0000000..c5d72d2 --- /dev/null +++ b/frontend/pweb/lib/providers/recipient.dart @@ -0,0 +1,80 @@ +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/recipient/filter.dart'; +import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/models/recipient/status.dart'; + +import 'package:pweb/services/recipient/recipient.dart'; + + +class RecipientProvider extends ChangeNotifier { + final RecipientService _service; + + RecipientProvider(this._service); + + List _recipients = []; + bool _isLoading = false; + String? _error; + RecipientFilter _selectedFilter = RecipientFilter.all; + String _query = ''; + + Recipient? _selectedRecipient; + + List get recipients => _recipients; + bool get isLoading => _isLoading; + String? get error => _error; + RecipientFilter get selectedFilter => _selectedFilter; + String get query => _query; + Recipient? get selectedRecipient => _selectedRecipient; + + List get filteredRecipients { + List filtered = _recipients.where((r) { + switch (_selectedFilter) { + case RecipientFilter.ready: + return r.status == RecipientStatus.ready; + case RecipientFilter.registered: + return r.status == RecipientStatus.registered; + case RecipientFilter.notRegistered: + return r.status == RecipientStatus.notRegistered; + case RecipientFilter.all: + return true; + } + }).toList(); + + if (_query.isNotEmpty) { + filtered = filtered.where((r) => r.matchesQuery(_query)).toList(); + } + + return filtered; + } + + Future loadRecipients() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _recipients = await _service.fetchRecipients(); + } catch (e) { + _error = e.toString(); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + void setFilter(RecipientFilter filter) { + _selectedFilter = filter; + notifyListeners(); + } + + void setQuery(String query) { + _query = query.trim().toLowerCase(); + notifyListeners(); + } + + void selectRecipient(Recipient? recipient) { + _selectedRecipient = recipient; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/providers/template.dart b/frontend/pweb/lib/providers/template.dart new file mode 100644 index 0000000..4f5c4c2 --- /dev/null +++ b/frontend/pweb/lib/providers/template.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + + +class FutureProviderTemplate extends ChangeNotifier { + FutureProviderTemplate({required this.loader}); + + final Future Function() loader; + + T? _data; + bool _isLoading = false; + String? _error; + + T? get data => _data; + bool get isLoading => _isLoading; + String? get error => _error; + + Future load() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _data = await loader(); + } catch (e) { + _error = e.toString(); + } + + _isLoading = false; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart new file mode 100644 index 0000000..c5a74e4 --- /dev/null +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/services/auth.dart'; + + +class TwoFactorProvider extends ChangeNotifier { + final AuthenticationService _authService; + + TwoFactorProvider(this._authService); + + bool _isSubmitting = false; + bool _hasError = false; + bool _verificationSuccess = false; + + bool get isSubmitting => _isSubmitting; + bool get hasError => _hasError; + bool get verificationSuccess => _verificationSuccess; + + + Future submitCode(String code) async { + _isSubmitting = true; + _hasError = false; + _verificationSuccess = false; + notifyListeners(); + + try { + final success = await _authService.verifyTwoFactorCode(code); + if (success) { + _verificationSuccess = true; + } + } catch (e) { + _hasError = true; + } finally { + _isSubmitting = false; + notifyListeners(); + } + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/providers/upload_history.dart b/frontend/pweb/lib/providers/upload_history.dart new file mode 100644 index 0000000..02a8325 --- /dev/null +++ b/frontend/pweb/lib/providers/upload_history.dart @@ -0,0 +1,10 @@ +import 'package:pshared/models/payment/upload_history_item.dart'; + +import 'package:pweb/providers/template.dart'; +import 'package:pweb/services/payments/upload_history.dart'; + + +class UploadHistoryProvider extends FutureProviderTemplate> { + UploadHistoryProvider({required UploadHistoryService service}) + : super(loader: service.fetchHistory); +} \ No newline at end of file diff --git a/frontend/pweb/lib/providers/wallets.dart b/frontend/pweb/lib/providers/wallets.dart new file mode 100644 index 0000000..f7c16f4 --- /dev/null +++ b/frontend/pweb/lib/providers/wallets.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/models/wallet.dart'; +import 'package:pweb/services/wallets.dart'; + + +class WalletsProvider with ChangeNotifier { + final WalletsService _service; + + WalletsProvider(this._service); + + List? _wallets; + bool _isLoading = false; + String? _error; + Wallet? _selectedWallet; + bool _isHidden = true; + + List? get wallets => _wallets; + bool get isLoading => _isLoading; + String? get error => _error; + Wallet? get selectedWallet => _selectedWallet; + bool get isHidden => _isHidden; + + + + + void selectWallet(Wallet wallet) { + _selectedWallet = wallet; + notifyListeners(); + } + + Future loadData() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _wallets = await _service.getWallets(); + } catch (e) { + _error = e.toString(); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + Future getWalletById(String walletId) async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final wallet = await _service.getWallet(walletId); + return wallet; + } catch (e) { + _error = e.toString(); + return null; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + void updateName(String walletRef, String newName) { + final index = _wallets?.indexWhere((w) => w.id == walletRef); + if (index != null && index >= 0) { + _wallets![index] = _wallets![index].copyWith(name: newName); + notifyListeners(); + } + } + + + void updateBalance(String walletRef, double newBalance) { + final index = _wallets?.indexWhere((w) => w.id == walletRef); + if (index != null && index >= 0) { + _wallets![index] = _wallets![index].copyWith(balance: newBalance); + notifyListeners(); + } + } + + Future updateWallet(Wallet wallet) async { + try { + await _service.updateWallet(); + final index = _wallets?.indexWhere((w) => w.id == wallet.id); + if (index != null && index >= 0) { + _wallets![index] = wallet; + notifyListeners(); + } + } catch (e) { + _error = e.toString(); + notifyListeners(); + } + } + + + Future addWallet(Wallet wallet) async { + try { + final newWallet = await _service.createWallet(); // Pass the wallet parameter + _wallets = [...?_wallets, ]; // Add the new wallet + notifyListeners(); + } catch (e) { + _error = e.toString(); + notifyListeners(); + } + } + + Future deleteWallet(String walletId) async { + try { + await _service.deleteWallet(); // Pass the walletId parameter + _wallets?.removeWhere((w) => w.id == walletId); + notifyListeners(); + } catch (e) { + _error = e.toString(); + notifyListeners(); + } + } + + void toggleVisibility(String walletId) { + final index = _wallets?.indexWhere((w) => w.id == walletId); + if (index != null && index >= 0) { + final wallet = _wallets![index]; + _wallets![index] = wallet.copyWith(isHidden: !wallet.isHidden); + notifyListeners(); + } + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/services/amplitude.dart b/frontend/pweb/lib/services/amplitude.dart new file mode 100644 index 0000000..c1384f4 --- /dev/null +++ b/frontend/pweb/lib/services/amplitude.dart @@ -0,0 +1,210 @@ +import 'package:amplitude_flutter/amplitude.dart'; +import 'package:amplitude_flutter/configuration.dart'; +import 'package:amplitude_flutter/constants.dart' as amp; +import 'package:amplitude_flutter/events/base_event.dart'; +import 'package:flutter/widgets.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/status.dart'; +import 'package:pshared/models/recipient/type.dart'; +import 'package:pweb/widgets/sidebar/destinations.dart'; + + +class AmplitudeService { + static late Amplitude _analytics; + + static Amplitude _amp() => _analytics; + + static Future initialize() async { + _analytics = Amplitude(Configuration( + apiKey: '12345', //TODO define through App Contants + serverZone: amp.ServerZone.eu, //TODO define through App Contants + )); + await _analytics.isBuilt; + } + + static Future identify(Account account) async => + _amp().setUserId(account.id); + + + static Future login(Account account) async => + _logEvent( + 'login', + userProperties: { + // 'email': account.email, TODO Add email into account + 'locale': account.locale, + }, + ); + + static Future logout() async => _logEvent("logout"); + + static Future pageOpened(PayoutDestination page, {String? path, String? uiSource}) async { + return _logEvent("pageOpened", eventProperties: { + "page": page, + if (path != null) "path": path, + if (uiSource != null) "uiSource": uiSource, + }); + } + + //TODO Add when registration is ready. User properties {user_id, registration_date, has_wallet (true/false), wallet_balance (should concider loggin it as: 0 / <100 / 100–500 / 500+), preferred_method (Wallet/Card/Bank/IBAN), total_transactions, total_amount, last_payout_date, last_login_date , marketing_source} + + // static Future registrationStarted(String method, String country) async => + // _logEvent("registrationStarted", eventProperties: {"method": method, "country": country}); + + // static Future registrationCompleted(String method, String country) async => + // _logEvent("registrationCompleted", eventProperties: {"method": method, "country": country}); + + static Future pageNotFound(String url) async => + _logEvent("pageNotFound", eventProperties: {"url": url}); + + static Future localeChanged(Locale locale) async => + _logEvent("localeChanged", eventProperties: {"locale": locale.toString()}); + + static Future localeMatched(String locale, bool haveRequested) async => //DO we need it? + _logEvent("localeMatched", eventProperties: { + "locale": locale, + "have_requested_locale": haveRequested + }); + + static Future recipientAddStarted() async => + _logEvent("recipientAddStarted"); + + static Future recipientAddCompleted( + RecipientType type, + RecipientStatus status, + Set methods, + ) async { + _logEvent( + "recipientAddCompleted", + eventProperties: { + "methods": methods.map((m) => m.name).toList(), + "type": type.name, + "status": status.name, + }, + ); + } + + static Future _paymentEvent( + String evt, + double amount, + double fee, + bool payerCoversFee, + PaymentType source, + PaymentType recpientPaymentMethod, { + String? message, + String? errorType, + Map? extraProps, + }) async { + final props = { + "amount": amount, + "fee": fee, + "feeCoveredBy": payerCoversFee ? 'payer' : 'recipient', + "source": source, + "recipient_method": recpientPaymentMethod, + if (message != null) "message": message, + if (errorType != null) "error_type": errorType, + if (extraProps != null) ...extraProps, + }; + return _logEvent(evt, eventProperties: props); + } + + static Future paymentPrepared(double amount, double fee, + bool payerCoversFee, PaymentType source, PaymentType recpientPaymentMethod) async => + _paymentEvent("paymentPrepared", amount, fee, payerCoversFee, source, recpientPaymentMethod); + //TODO Rework paymentStarted (do i need all those properties or is the event enough? Mb properties should be passed at paymentPrepared) + static Future paymentStarted(double amount, double fee, + bool payerCoversFee, PaymentType source, PaymentType recpientPaymentMethod) async => + _paymentEvent("paymentStarted", amount, fee, payerCoversFee, source, recpientPaymentMethod); + + static Future paymentFailed(double amount, double fee, bool payerCoversFee, + PaymentType source, PaymentType recpientPaymentMethod, String errorType, String message) async => + _paymentEvent("paymentFailed", amount, fee, payerCoversFee, source, recpientPaymentMethod, + errorType: errorType, message: message); + + static Future paymentError(double amount, double fee, bool payerCoversFee, + PaymentType source,PaymentType recpientPaymentMethod, String message) async => + _paymentEvent("paymentError", amount, fee, payerCoversFee, source, recpientPaymentMethod, + message: message); + + static Future paymentSuccess({ + required double amount, + required double fee, + required bool payerCoversFee, + required PaymentType source, + required PaymentType recpientPaymentMethod, + required String transactionId, + String? comment, + required int durationMs, + }) async { + return _paymentEvent( + "paymentSuccess", + amount, + fee, + payerCoversFee, + source, + recpientPaymentMethod, + message: comment, + extraProps: { + "transaction_id": transactionId, + "duration_ms": durationMs, //How do i calculate duration here? + "\$revenue": amount, //How do i calculate revenue here? + "\$revenueType": "payment", //Do we need to get revenue type? + }, + ); + } + + //TODO add when support is ready + // static Future supportOpened(String fromPage, String trigger) async => + // _logEvent("supportOpened", eventProperties: {"from_page": fromPage, "trigger": trigger}); + + // static Future supportMessageSent(String category, bool resolved) async => + // _logEvent("supportMessageSent", eventProperties: {"category": category, "resolved": resolved}); + + + static Future walletTopUp(double amount, PaymentType method) async => + _logEvent("walletTopUp", eventProperties: {"amount": amount, "method": method}); + + + //TODO Decide do we need uiElementClicked or pageOpened is enough? + static Future uiElementClicked(String elementName, String page, String uiSource) async => + _logEvent("uiElementClicked", eventProperties: { + "element_name": elementName, + "page": page, + "uiSource": uiSource + }); + + static final Map _stepStartTimes = {}; + //TODO Consider it as part of payment flow or registration flow or adding recipient and rework accordingly + static Future stepStarted(String stepName, {String? context}) async { + _stepStartTimes[stepName] = DateTime.now().millisecondsSinceEpoch; + return _logEvent("stepStarted", eventProperties: { + "step_name": stepName, + if (context != null) "context": context, + }); + } + + static Future stepCompleted(String stepName, bool success) async { + final now = DateTime.now().millisecondsSinceEpoch; + final start = _stepStartTimes[stepName] ?? now; + final duration = now - start; + return _logEvent("stepCompleted", eventProperties: { + "step_name": stepName, + "duration_ms": duration, + "success": success + }); + } + + static Future _logEvent( + String eventType, { + Map? eventProperties, + Map? userProperties, + }) async { + final event = BaseEvent( + eventType, + eventProperties: eventProperties, + userProperties: userProperties, + ); + _amp().track(event); + print(event.toString()); //TODO delete when everything is ready + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/services/auth.dart b/frontend/pweb/lib/services/auth.dart new file mode 100644 index 0000000..95a75e7 --- /dev/null +++ b/frontend/pweb/lib/services/auth.dart @@ -0,0 +1,12 @@ + +class AuthenticationService { + Future verifyTwoFactorCode(String code) async { + await Future.delayed(const Duration(seconds: 2)); + + if (code == '000000') { + return true; + } else { + throw Exception('Wrong Code'); //TODO Localize + } + } +} diff --git a/frontend/pweb/lib/services/balance.dart b/frontend/pweb/lib/services/balance.dart new file mode 100644 index 0000000..8587afa --- /dev/null +++ b/frontend/pweb/lib/services/balance.dart @@ -0,0 +1,22 @@ +abstract class BalanceService { + Future getBalance(); + Future getWalletName(); + Future getWalletId(); +} + +class MockBalanceService implements BalanceService { + @override + Future getBalance() async { + return 3000000.56; + } + + @override + Future getWalletName() async { + return "Wallet"; + } + + @override + Future getWalletId() async { + return "WA-12345667"; + } +} diff --git a/frontend/pweb/lib/services/payments/payment_methods.dart b/frontend/pweb/lib/services/payments/payment_methods.dart new file mode 100644 index 0000000..5946b93 --- /dev/null +++ b/frontend/pweb/lib/services/payments/payment_methods.dart @@ -0,0 +1,42 @@ +import 'package:pshared/models/payment/methods/type.dart'; + +import 'package:pshared/models/payment/type.dart'; + + +abstract class PaymentMethodsService { + Future> fetchMethods(); +} + +class MockPaymentMethodsService implements PaymentMethodsService { + @override + Future> fetchMethods() async { + await Future.delayed(const Duration(milliseconds: 200)); + return [ + PaymentMethod( + id: '1', + label: 'My account', + details: '•••4567', + type: PaymentType.bankAccount, + isMain: true, + ), + PaymentMethod( + id: '2', + label: 'Euro IBAN', + details: 'DE•• •••8901', + type: PaymentType.iban, + ), + PaymentMethod( + id: '3', + label: 'Wallet', + details: 'WA‑12345667', + type: PaymentType.wallet, + ), + PaymentMethod( + id: '4', + label: 'Credit Card', + details: '21•• •••• •••• 8901', + type: PaymentType.card, + ), + ]; + } +} diff --git a/frontend/pweb/lib/services/payments/upload_history.dart b/frontend/pweb/lib/services/payments/upload_history.dart new file mode 100644 index 0000000..a4e1285 --- /dev/null +++ b/frontend/pweb/lib/services/payments/upload_history.dart @@ -0,0 +1,20 @@ +import 'package:pshared/models/payment/upload_history_item.dart'; + + +abstract class UploadHistoryService { + Future> fetchHistory(); +} + +class MockUploadHistoryService implements UploadHistoryService { + @override + Future> fetchHistory() async { + await Future.delayed(const Duration(milliseconds: 300)); + + return [ + UploadHistoryItem(name: "cards_payout_single.csv", status: "Valid", time: "5 hours ago"), + UploadHistoryItem(name: "rfba_norm.csv", status: "Valid", time: "Yesterday"), + UploadHistoryItem(name: "iban (4).csv", status: "Valid", time: "Yesterday"), + UploadHistoryItem(name: "rfba_wrong.csv", status: "Error", time: "2 days ago"), + ]; + } +} diff --git a/frontend/pweb/lib/services/recipient/recipient.dart b/frontend/pweb/lib/services/recipient/recipient.dart new file mode 100644 index 0000000..7c4cc68 --- /dev/null +++ b/frontend/pweb/lib/services/recipient/recipient.dart @@ -0,0 +1,88 @@ +import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/models/payment/methods/card.dart'; +import 'package:pshared/models/payment/methods/iban.dart'; +import 'package:pshared/models/payment/methods/russian_bank.dart'; +import 'package:pshared/models/payment/methods/wallet.dart'; +import 'package:pshared/models/recipient/status.dart'; +import 'package:pshared/models/recipient/type.dart'; + + +class RecipientService { + Future> fetchRecipients() async { + await Future.delayed(const Duration(milliseconds: 500)); + return RecipientMockData.all; + } +} + +class RecipientMockData { + static List get all => [ + Recipient.mock( + name: 'Alice Johnson', + email: 'alice@example.com', + status: RecipientStatus.ready, + type: RecipientType.internal, + card: CardPaymentMethod( + pan: '1213', + firstName: 'Alice', + lastName: 'Johnson', + ), + ), + Recipient.mock( + name: 'Bob & Co Ltd.', + email: 'payout@bobco.com', + status: RecipientStatus.registered, + type: RecipientType.external, + card: CardPaymentMethod( + pan: '4343', + firstName: 'Bob', + lastName: 'Co', + ), + iban: IbanPaymentMethod( + iban: 'FR7630***890189', + accountHolder: 'Bob & Co Ltd.', + bic: 'AGRIFRPP', + bankName: 'Credit Agricole', + ), + wallet: WalletPaymentMethod(walletId: '8932231'), + ), + Recipient.mock( + name: 'Carlos Kline', + email: 'carlos@acme.org', + status: RecipientStatus.notRegistered, + type: RecipientType.internal, + wallet: WalletPaymentMethod(walletId: '7723490'), + ), + Recipient.mock( + name: 'Delta Outsourcing GmbH', + email: 'finance@delta-os.de', + status: RecipientStatus.registered, + type: RecipientType.external, + card: CardPaymentMethod( + pan: '9988', + firstName: 'Delta', + lastName: 'GmbH', + ), + iban: IbanPaymentMethod( + iban: 'DE4450***324931', + accountHolder: 'Delta Outsourcing GmbH', + bic: 'INGDDEFFXXX', + bankName: 'ING', + ), + ), + Recipient.mock( + name: 'Erin Patel', + email: 'erin@labster.io', + status: RecipientStatus.ready, + type: RecipientType.internal, + bank: RussianBankAccountPaymentMethod( + accountNumber: '4081***7654', + recipientName: 'Erin Patel', + inn: '7812012345', + kpp: '781201001', + bankName: 'Alfa-Bank', + bik: '044525593', + correspondentAccount: '30101810200000000593', + ), + ), + ]; +} diff --git a/frontend/pweb/lib/services/wallets.dart b/frontend/pweb/lib/services/wallets.dart new file mode 100644 index 0000000..7f5d033 --- /dev/null +++ b/frontend/pweb/lib/services/wallets.dart @@ -0,0 +1,41 @@ +import 'package:pweb/models/currency.dart'; +import 'package:pweb/models/wallet.dart'; + + +abstract class WalletsService { + Future> getWallets(); + Future> updateWallet(); + Future> createWallet(); + Future> deleteWallet(); + + Future getWallet(String walletRef); +} + +class MockWalletsService implements WalletsService { + final List _wallets = [ + Wallet(id: '1124', walletUserID: 'WA-12345667', name: 'Main Wallet', balance: 10000000.0, currency: Currency.rub), + Wallet(id: '2124', walletUserID: 'WA-76654321', name: 'Savings', balance: 2500.5, currency: Currency.usd), + ]; + + @override + Future> getWallets() async { + return _wallets; + } + + @override + Future getWallet(String walletId) async { + return _wallets.firstWhere( + (wallet) => wallet.id == walletId, + orElse: () => throw Exception('Wallet not found'), + ); + } + + @override + Future> updateWallet() async => []; + + @override + Future> createWallet() async => []; + + @override + Future> deleteWallet() async => []; +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/clipboard.dart b/frontend/pweb/lib/utils/clipboard.dart new file mode 100644 index 0000000..9b71a63 --- /dev/null +++ b/frontend/pweb/lib/utils/clipboard.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:pshared/utils/snackbar.dart'; + + +Future copyToClipboard(BuildContext context, String text, String hint, {int delaySeconds = 3}) async { + final res = Clipboard.setData(ClipboardData(text: text)); + notifyUser(context, hint, delaySeconds: delaySeconds); + return res; +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/currency.dart b/frontend/pweb/lib/utils/currency.dart new file mode 100644 index 0000000..7f9a33d --- /dev/null +++ b/frontend/pweb/lib/utils/currency.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/models/currency.dart'; + + +String currencyCodeToSymbol(Currency currencyCode) { + switch (currencyCode) { + case Currency.usd: + return '\$'; + case Currency.eur: + return '€'; + case Currency.rub: + return '₽'; + case Currency.usdt: + return 'USDT'; + case Currency.usdc: + return 'USDC'; + } +} + +String currencyToString(Currency currencyCode, double amount) { + return '${amount.toStringAsFixed(2)} ${currencyCodeToSymbol(currencyCode)}'; +} + +IconData iconForCurrencyType(Currency currencyCode) { + switch (currencyCode) { + case Currency.usd: + return Icons.currency_exchange; + case Currency.eur: + return Icons.currency_exchange; + case Currency.rub: + return Icons.currency_ruble; + case Currency.usdt: + return Icons.currency_exchange; + case Currency.usdc: + return Icons.money; + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/dimensions.dart b/frontend/pweb/lib/utils/dimensions.dart new file mode 100644 index 0000000..cb7ff4a --- /dev/null +++ b/frontend/pweb/lib/utils/dimensions.dart @@ -0,0 +1,48 @@ +class AppDimensions { + final double paddingSmall; + final double paddingMedium; + final double paddingLarge; + final double paddingXLarge; + final double paddingXXLarge; + final double paddingXXXLarge; + + final double spacingSmall; + + final double borderRadiusSmall; + final double borderRadiusMedium; + + final double maxContentWidth; + final double buttonWidth; + final double buttonHeight; + + final double iconSizeLarge; + final double iconSizeMedium; + final double iconSizeSmall; + + final double elevationSmall; + + const AppDimensions({ + this.paddingSmall = 8, + this.paddingMedium = 12, + this.paddingLarge = 16, + this.paddingXLarge = 20, + this.paddingXXLarge = 25, + this.paddingXXXLarge = 30, + + this.spacingSmall = 5, + + + this.borderRadiusSmall = 12, + this.borderRadiusMedium = 16, + + this.maxContentWidth = 500, + this.buttonWidth = 300, + this.buttonHeight = 40, + + this.iconSizeLarge = 30, + this.iconSizeMedium = 24, + this.iconSizeSmall = 20, + + this.elevationSmall = 4, + }); +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/error/content.dart b/frontend/pweb/lib/utils/error/content.dart new file mode 100644 index 0000000..35fb7a1 --- /dev/null +++ b/frontend/pweb/lib/utils/error/content.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + + +class ErrorSnackBarContent extends StatelessWidget { + final String situation; + final String localizedError; + + const ErrorSnackBarContent({ + super.key, + required this.situation, + required this.localizedError, + }); + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, // wrap to content + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + situation, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text(localizedError), + ], + ); +} diff --git a/frontend/pweb/lib/utils/error/handler.dart b/frontend/pweb/lib/utils/error/handler.dart new file mode 100644 index 0000000..2313b4f --- /dev/null +++ b/frontend/pweb/lib/utils/error/handler.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/api/responses/error/connectivity.dart'; +import 'package:pshared/api/responses/error/server.dart'; +import 'package:pshared/config/constants.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ErrorHandler { + /// A mapping of server-side error codes to localized user-friendly messages. + /// Update these keys to match the 'ErrorResponse.Error' field in your Go code. + static Map getErrorMessagesLocs(AppLocalizations locs) { + return { + 'account_not_verified': locs.errorAccountNotVerified, + 'unauthorized': locs.errorLoginUnauthorized, + 'verification_token_not_found': locs.errorVerificationTokenNotFound, + 'internal_error': locs.errorInternalError, + + 'data_conflict': locs.errorDataConflict, + 'access_denied': locs.errorAccessDenied, + 'broken_payload': locs.errorBrokenPayload, + 'invalid_argument': locs.errorInvalidArgument, + 'broken_reference': locs.errorBrokenReference, + 'invalid_query_parameter': locs.errorInvalidQueryParameter, + 'not_implemented': locs.errorNotImplemented, + 'license_required': locs.errorLicenseRequired, + 'not_found': locs.errorNotFound, + 'name_missing': locs.errorNameMissing, + 'email_missing': locs.errorEmailMissing, + 'password_missing': locs.errorPasswordMissing, + 'email_not_registered': locs.errorEmailNotRegistered, + 'duplicate_email': locs.errorDuplicateEmail, + }; + } + + static Map getErrorMessages(BuildContext context) { + return getErrorMessagesLocs(AppLocalizations.of(context)!); + } + + /// Determine which handler to use based on the runtime type of [e]. + /// If no match is found, just return the error’s string representation. + static String handleError(BuildContext context, Object e) { + return handleErrorLocs(AppLocalizations.of(context)!, e); + } + + static String handleErrorLocs(AppLocalizations locs, Object e) { + final errorHandlers = { + ErrorResponse: (ex) => _handleErrorResponseLocs(locs, ex as ErrorResponse), + ConnectivityError: (ex) => _handleConnectivityErrorLocs(locs, ex as ConnectivityError), + }; + + return errorHandlers[e.runtimeType]?.call(e) ?? e.toString(); + } + + static String _handleErrorResponseLocs(AppLocalizations locs, ErrorResponse e) { + final errorMessages = getErrorMessagesLocs(locs); + // Return the localized message if we recognize the error key, else use the raw details + return errorMessages[e.error] ?? e.details; + } + + /// Handler for connectivity issues. + static String _handleConnectivityErrorLocs(AppLocalizations locs, ConnectivityError e) { + return locs.connectivityError(Constants.serviceUrl); + } +} diff --git a/frontend/pweb/lib/utils/error/snackbar.dart b/frontend/pweb/lib/utils/error/snackbar.dart new file mode 100644 index 0000000..ca98a9d --- /dev/null +++ b/frontend/pweb/lib/utils/error/snackbar.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:pweb/utils/error/handler.dart'; +import 'package:pweb/widgets/error/content.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +ScaffoldFeatureController notifyUserOfErrorX({ + required ScaffoldMessengerState scaffoldMessenger, + required String errorSituation, + required Object exception, + required AppLocalizations appLocalizations, + int delaySeconds = 3, +}) { + // A. Localized user-friendly error message + final String localizedError = ErrorHandler.handleErrorLocs(appLocalizations, exception); + + // B. Technical details for advanced reference + final String technicalDetails = exception.toString(); + + // C. Build the snack bar + final snackBar = _buildMainErrorSnackBar( + errorSituation: errorSituation, + localizedError: localizedError, + technicalDetails: technicalDetails, + loc: appLocalizations, + scaffoldMessenger: scaffoldMessenger, + delaySeconds: delaySeconds, + ); + + // D. Show it + return scaffoldMessenger.showSnackBar(snackBar); +} + +ScaffoldFeatureController notifyUserOfError({ + required BuildContext context, + required String errorSituation, + required Object exception, + int delaySeconds = 3, +}) => notifyUserOfErrorX( + scaffoldMessenger: ScaffoldMessenger.of(context), + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, +); + +Future executeActionWithNotification({ + required BuildContext context, + required Future Function() action, + required String errorMessage, + int delaySeconds = 3, +}) async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final localizations = AppLocalizations.of(context)!; + + try { + return await action(); + } catch (e) { + // Report the error using your existing notifier. + notifyUserOfErrorX( + scaffoldMessenger: scaffoldMessenger, + errorSituation: errorMessage, + exception: e, + appLocalizations: localizations, + delaySeconds: delaySeconds, + ); + } + return null; +} + +Future> postNotifyUserOfError({ + required ScaffoldMessengerState scaffoldMessenger, + required String errorSituation, + required Object exception, + required AppLocalizations appLocalizations, + int delaySeconds = 3, +}) { + + final completer = Completer>(); + + WidgetsBinding.instance.addPostFrameCallback((_) => completer.complete(notifyUserOfErrorX( + scaffoldMessenger: scaffoldMessenger, + errorSituation: errorSituation, + exception: exception, + appLocalizations: appLocalizations, + delaySeconds: delaySeconds, + )), + ); + + return completer.future; +} + +Future> postNotifyUserOfErrorX({ + required BuildContext context, + required String errorSituation, + required Object exception, + int delaySeconds = 3, +}) => postNotifyUserOfError( + scaffoldMessenger: ScaffoldMessenger.of(context), + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, +); + + +/// 2) A helper function that returns the main SnackBar widget +SnackBar _buildMainErrorSnackBar({ + required String errorSituation, + required String localizedError, + required String technicalDetails, + required AppLocalizations loc, + required ScaffoldMessengerState scaffoldMessenger, + int delaySeconds = 3, +}) => SnackBar( + duration: Duration(seconds: delaySeconds), + content: ErrorSnackBarContent( + situation: errorSituation, + localizedError: localizedError, + ), + action: SnackBarAction( + label: loc.showDetailsAction, + onPressed: () => scaffoldMessenger.showSnackBar(SnackBar( + content: Text(technicalDetails), + duration: const Duration(seconds: 6), + )), + ), +); diff --git a/frontend/pweb/lib/utils/error_handler.dart b/frontend/pweb/lib/utils/error_handler.dart new file mode 100644 index 0000000..2313b4f --- /dev/null +++ b/frontend/pweb/lib/utils/error_handler.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/api/responses/error/connectivity.dart'; +import 'package:pshared/api/responses/error/server.dart'; +import 'package:pshared/config/constants.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ErrorHandler { + /// A mapping of server-side error codes to localized user-friendly messages. + /// Update these keys to match the 'ErrorResponse.Error' field in your Go code. + static Map getErrorMessagesLocs(AppLocalizations locs) { + return { + 'account_not_verified': locs.errorAccountNotVerified, + 'unauthorized': locs.errorLoginUnauthorized, + 'verification_token_not_found': locs.errorVerificationTokenNotFound, + 'internal_error': locs.errorInternalError, + + 'data_conflict': locs.errorDataConflict, + 'access_denied': locs.errorAccessDenied, + 'broken_payload': locs.errorBrokenPayload, + 'invalid_argument': locs.errorInvalidArgument, + 'broken_reference': locs.errorBrokenReference, + 'invalid_query_parameter': locs.errorInvalidQueryParameter, + 'not_implemented': locs.errorNotImplemented, + 'license_required': locs.errorLicenseRequired, + 'not_found': locs.errorNotFound, + 'name_missing': locs.errorNameMissing, + 'email_missing': locs.errorEmailMissing, + 'password_missing': locs.errorPasswordMissing, + 'email_not_registered': locs.errorEmailNotRegistered, + 'duplicate_email': locs.errorDuplicateEmail, + }; + } + + static Map getErrorMessages(BuildContext context) { + return getErrorMessagesLocs(AppLocalizations.of(context)!); + } + + /// Determine which handler to use based on the runtime type of [e]. + /// If no match is found, just return the error’s string representation. + static String handleError(BuildContext context, Object e) { + return handleErrorLocs(AppLocalizations.of(context)!, e); + } + + static String handleErrorLocs(AppLocalizations locs, Object e) { + final errorHandlers = { + ErrorResponse: (ex) => _handleErrorResponseLocs(locs, ex as ErrorResponse), + ConnectivityError: (ex) => _handleConnectivityErrorLocs(locs, ex as ConnectivityError), + }; + + return errorHandlers[e.runtimeType]?.call(e) ?? e.toString(); + } + + static String _handleErrorResponseLocs(AppLocalizations locs, ErrorResponse e) { + final errorMessages = getErrorMessagesLocs(locs); + // Return the localized message if we recognize the error key, else use the raw details + return errorMessages[e.error] ?? e.details; + } + + /// Handler for connectivity issues. + static String _handleConnectivityErrorLocs(AppLocalizations locs, ConnectivityError e) { + return locs.connectivityError(Constants.serviceUrl); + } +} diff --git a/frontend/pweb/lib/utils/flagged_locale.dart b/frontend/pweb/lib/utils/flagged_locale.dart new file mode 100644 index 0000000..ad55a78 --- /dev/null +++ b/frontend/pweb/lib/utils/flagged_locale.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:intl/intl.dart'; + +import 'package:country_flags/country_flags.dart'; + + +String _locale2Flag(Locale l) { + if (l.languageCode == 'en') { + return 'gb'; + } + if (l.languageCode == 'uk') { + return 'ua'; + } + if (l.languageCode == 'el') { + return 'gr'; + } + return l.languageCode; +} + +final Map localeNames = { + 'en': 'English', + 'es': 'Español', + 'fr': 'Français', + 'de': 'Deutsch', + 'uk': 'Українська', + 'el': 'Ελληνικά', + 'ru': 'Русский', + 'pt': 'Português', + 'pl': 'Polski', + 'it': 'Italiano', + 'nl': 'Nederlands', +}; + +Widget getCountryFlag(Locale locale) { + return + CountryFlag.fromCountryCode( + _locale2Flag(locale), + height: 24, + width: 30, + shape: Rectangle(), + ); +} + +String getLocaleName(Locale locale) { + return localeNames[locale.languageCode] ?? Intl.canonicalizedLocale(locale.toString()).toUpperCase(); +} + +Widget getFlaggedLocale(Locale locale) { + return ListTile( + leading: getCountryFlag(locale), + title: Text(getLocaleName(locale), overflow: TextOverflow.ellipsis), + ); +} + diff --git a/frontend/pweb/lib/utils/http.dart b/frontend/pweb/lib/utils/http.dart new file mode 100644 index 0000000..7720fab --- /dev/null +++ b/frontend/pweb/lib/utils/http.dart @@ -0,0 +1,12 @@ +// ignore: avoid_web_libraries_in_flutter +import 'package:web/web.dart' as web; + + +String getUrl() { + return web.window.location.href; +} + +String? getQueryParameter(String queryParameter) { + Uri uri = Uri.parse(getUrl()); + return uri.queryParameters[queryParameter]; +} diff --git a/frontend/pweb/lib/utils/initials.dart b/frontend/pweb/lib/utils/initials.dart new file mode 100644 index 0000000..de7c803 --- /dev/null +++ b/frontend/pweb/lib/utils/initials.dart @@ -0,0 +1,5 @@ +String getInitials(String name) { + final parts = name.trim().split(' '); + if (parts.length == 1) return parts[0][0].toUpperCase(); + return (parts[0][0] + parts[1][0]).toUpperCase(); +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/notify.dart b/frontend/pweb/lib/utils/notify.dart new file mode 100644 index 0000000..3703111 --- /dev/null +++ b/frontend/pweb/lib/utils/notify.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/utils/snackbar.dart'; +import 'package:pweb/widgets/error/snackbar.dart'; + + +Future invokeAndNotify( + BuildContext context, { + required Future Function() operation, + String? operationSuccess, + String? operationError, + void Function(Object)? onError, + void Function(T)? onSuccess, +}) async { + final sm = ScaffoldMessenger.of(context); + final locs = AppLocalizations.of(context)!; + try { + final res = await operation(); + if (operationSuccess != null) { + notifyUserX(sm, operationSuccess); + } + if (onSuccess != null) { + onSuccess(res); + } + } catch (e) { + notifyUserOfErrorX( + scaffoldMessenger: sm, + errorSituation: operationError ?? locs.errorInternalError, + exception: e, + appLocalizations: locs, + ); + if (onError != null) { + onError(e); + } + rethrow; + } +} diff --git a/frontend/pweb/lib/utils/payment/dropdown.dart b/frontend/pweb/lib/utils/payment/dropdown.dart new file mode 100644 index 0000000..c56bd8e --- /dev/null +++ b/frontend/pweb/lib/utils/payment/dropdown.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/type.dart'; + +import 'package:pweb/pages/payment_methods/icon.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentMethodDropdown extends StatefulWidget { + final List methods; + final ValueChanged onChanged; + final PaymentMethod? initialValue; + + const PaymentMethodDropdown({ + super.key, + required this.methods, + required this.onChanged, + this.initialValue, + }); + + @override + State createState() => _PaymentMethodDropdownState(); +} + +class _PaymentMethodDropdownState extends State { + late PaymentMethod _selectedMethod; + + @override + void initState() { + super.initState(); + _selectedMethod = widget.initialValue ?? widget.methods.first; + } + + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + dropdownColor: Theme.of(context).colorScheme.onSecondary, + value: _selectedMethod, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.whereGetMoney, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + items: widget.methods.map((method) { + return DropdownMenuItem( + value: method, + child: Row( + children: [ + Icon(iconForPaymentType(method.type), size: 20), + const SizedBox(width: 8), + Text('${method.label} (${method.details})'), + ], + ), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + setState(() => _selectedMethod = value); + widget.onChanged(value); + } + }, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/payment/label.dart b/frontend/pweb/lib/utils/payment/label.dart new file mode 100644 index 0000000..11b040b --- /dev/null +++ b/frontend/pweb/lib/utils/payment/label.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +String getPaymentTypeLabel(BuildContext context, PaymentType type) { + final l10n = AppLocalizations.of(context)!; + return switch (type) { + PaymentType.card => l10n.paymentTypeCard, + PaymentType.bankAccount => l10n.paymentTypeBankAccount, + PaymentType.iban => l10n.paymentTypeIban, + PaymentType.wallet => l10n.paymentTypeWallet, + }; +} diff --git a/frontend/pweb/lib/utils/payment/selector_type.dart b/frontend/pweb/lib/utils/payment/selector_type.dart new file mode 100644 index 0000000..0680012 --- /dev/null +++ b/frontend/pweb/lib/utils/payment/selector_type.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/utils/payment/label.dart'; + + +class PaymentTypeSelector extends StatelessWidget { + final Map availableTypes; + final PaymentType selectedType; + final ValueChanged onSelected; + + const PaymentTypeSelector({ + super.key, + required this.availableTypes, + required this.selectedType, + required this.onSelected, + }); + + static const double _chipSpacing = 12.0; + static const double _chipBorderRadius = 10.0; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Wrap( + spacing: _chipSpacing, + runSpacing: _chipSpacing, + children: availableTypes.keys.map((type) { + final isSelected = selectedType == type; + + return ChoiceChip( + label: Text( + getPaymentTypeLabel(context, type), + style: theme.textTheme.titleMedium!.copyWith( + color: isSelected + ? theme.colorScheme.onPrimary + : theme.colorScheme.onSurface, + ), + ), + selected: isSelected, + showCheckmark: false, + selectedColor: theme.colorScheme.primary, + backgroundColor: theme.colorScheme.onSecondary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(_chipBorderRadius), + ), + onSelected: (_) => onSelected(type), + ); + }).toList(), + ); + } +} diff --git a/frontend/pweb/lib/utils/share.dart b/frontend/pweb/lib/utils/share.dart new file mode 100644 index 0000000..1c3b0ed --- /dev/null +++ b/frontend/pweb/lib/utils/share.dart @@ -0,0 +1,40 @@ +// ignore: avoid_web_libraries_in_flutter +import 'package:web/web.dart' as web; + +import 'package:flutter/material.dart'; + +import 'package:share_plus/share_plus.dart'; + +import 'package:pweb/utils/clipboard.dart'; + + +enum DeviceType { desktop, mobile, unknown } + +DeviceType getDeviceType() { + final userAgent = web.window.navigator.userAgent; + if (userAgent.contains('Mobile') || userAgent.contains('Android') || userAgent.contains('iPhone')) { + return DeviceType.mobile; + } + + if (userAgent.contains('Windows') || userAgent.contains('Macintosh') || userAgent.contains('Linux')) { + return DeviceType.desktop; + } + + return DeviceType.unknown; +} + + + +Future share(BuildContext context, String content, String hint, String clipboardHint, {int delaySeconds = 1}) { + + if (getDeviceType() != DeviceType.desktop) { + final RenderBox box = context.findRenderObject() as RenderBox; + return SharePlus.instance.share(ShareParams( + text: content, + subject: hint, + sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size, + )); + } + + return copyToClipboard(context, content, clipboardHint, delaySeconds: delaySeconds); +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/snackbar.dart b/frontend/pweb/lib/utils/snackbar.dart new file mode 100644 index 0000000..a8524ed --- /dev/null +++ b/frontend/pweb/lib/utils/snackbar.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + + +ScaffoldFeatureController notifyUserX( + ScaffoldMessengerState sm, + String message, + { int delaySeconds = 3 } +) => sm.showSnackBar(SnackBar(content: Text(message), duration: Duration(seconds: delaySeconds))); + +ScaffoldFeatureController notifyUser( + BuildContext context, + String message, + { int delaySeconds = 3 } +) => notifyUserX(ScaffoldMessenger.of(context), message, delaySeconds: delaySeconds); + +Future> postNotifyUser( + BuildContext context, String message, {int delaySeconds = 3}) { + + final completer = Completer>(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final controller = notifyUser(context, message, delaySeconds: delaySeconds); + completer.complete(controller); + }); + + return completer.future; +} diff --git a/frontend/pweb/lib/utils/snapshot_haserror_check.dart b/frontend/pweb/lib/utils/snapshot_haserror_check.dart new file mode 100644 index 0000000..e3ac51a --- /dev/null +++ b/frontend/pweb/lib/utils/snapshot_haserror_check.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'package:logging/logging.dart'; + + +bool hasError(AsyncSnapshot snapshot, String source) { + if (snapshot.hasError) { + Logger(source).warning('Error occurred', snapshot.error?.toString(), StackTrace.current); + return true; + } + return false; +} \ No newline at end of file diff --git a/frontend/pweb/lib/utils/text_field_styles.dart b/frontend/pweb/lib/utils/text_field_styles.dart new file mode 100644 index 0000000..fb59df8 --- /dev/null +++ b/frontend/pweb/lib/utils/text_field_styles.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +TextStyle getTextFieldStyle(BuildContext context, bool isEditable) { + return TextStyle( + color: isEditable + ? Theme.of(context).shadowColor + : Theme.of(context).disabledColor + ); +} + +InputDecoration getInputDecoration(BuildContext context, String label, bool isEditable) { + final theme = Theme.of(context); + return InputDecoration( + labelText: label, + labelStyle: TextStyle( + color: isEditable ? theme.shadowColor : theme.disabledColor, + ) + ); +} diff --git a/frontend/pweb/lib/utils/time_ago.dart b/frontend/pweb/lib/utils/time_ago.dart new file mode 100644 index 0000000..5505b09 --- /dev/null +++ b/frontend/pweb/lib/utils/time_ago.dart @@ -0,0 +1,20 @@ +import 'package:flutter/cupertino.dart'; + +import 'package:timeago/timeago.dart' as timeago; + +import 'package:pshared/models/storable.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +String timeAgo(BuildContext context, Storable storable) { + // Use updatedAt if available; otherwise, fallback to createdAt. + final timestamp = storable.updatedAt.isAfter(storable.createdAt) + ? storable.updatedAt + : storable.createdAt; + final timestampPrefix = storable.updatedAt.isAfter(storable.createdAt) + ? AppLocalizations.of(context)!.edited + : AppLocalizations.of(context)!.created; + + return '$timestampPrefix ${timeago.format(timestamp)}'; +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/appbar/app_bar.dart b/frontend/pweb/lib/widgets/appbar/app_bar.dart new file mode 100644 index 0000000..d5c8299 --- /dev/null +++ b/frontend/pweb/lib/widgets/appbar/app_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/appbar/profile.dart'; +import 'package:pweb/widgets/logo.dart'; + + +class PayoutAppBar extends StatelessWidget implements PreferredSizeWidget { + const PayoutAppBar({ + super.key, + required this.title, + required this.onAddFundsPressed, + this.actions, + this.onLogout, + this.avatarUrl, + }); + + final Widget title; + final VoidCallback onAddFundsPressed; + final List? actions; + final VoidCallback? onLogout; + final String? avatarUrl; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(left: 110, right: 80), + child: AppBar( + automaticallyImplyLeading: false, + title: Row( + children: [ + ServiceLogo(), + SizedBox(width: 16), + title, + ], + ), + // leading: Padding(padding: EdgeInsetsGeometry.symmetric(horizontal: 8, vertical: 8), child: ServiceLogo()), + actions: [ + ProfileAvatar( + avatarUrl: avatarUrl, + onLogout: onLogout, + ), + const SizedBox(width: 8), + ], + ), + ); +} diff --git a/frontend/pweb/lib/widgets/appbar/notifications.dart b/frontend/pweb/lib/widgets/appbar/notifications.dart new file mode 100644 index 0000000..e2c2000 --- /dev/null +++ b/frontend/pweb/lib/widgets/appbar/notifications.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class NotificationsButton extends StatelessWidget { + const NotificationsButton({super.key}); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(Icons.notifications), + onPressed: null, + ); + } +} diff --git a/frontend/pweb/lib/widgets/appbar/profile.dart b/frontend/pweb/lib/widgets/appbar/profile.dart new file mode 100644 index 0000000..365395c --- /dev/null +++ b/frontend/pweb/lib/widgets/appbar/profile.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ProfileAvatar extends StatelessWidget { + const ProfileAvatar({super.key, this.avatarUrl, this.onLogout}); + + final String? avatarUrl; + final VoidCallback? onLogout; + + @override + Widget build(BuildContext context) => PopupMenuButton( + tooltip: AppLocalizations.of(context)!.profile, + onSelected: (value) { + if (value == 1) onLogout?.call(); + }, + itemBuilder: (_) => [ + PopupMenuItem( + value: 1, + child: Row( + children: [ + Icon( + Icons.logout, + size: 20, + color: Theme.of(context).iconTheme.color, + ), + const SizedBox(width: 8), + Text(AppLocalizations.of(context)!.logout), + ], + ), + ), + ], + child: CircleAvatar( + radius: 16, + foregroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, + child: avatarUrl == null ? const Icon(Icons.person, size: 24) : null, + ), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/constrained_form.dart b/frontend/pweb/lib/widgets/constrained_form.dart new file mode 100644 index 0000000..b01c584 --- /dev/null +++ b/frontend/pweb/lib/widgets/constrained_form.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + + +class ConstrainedForm extends StatelessWidget { + final GlobalKey formKey; + final List children; + final AutovalidateMode? autovalidateMode; + + const ConstrainedForm({ + super.key, + required this.formKey, + required this.children, + this.autovalidateMode, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Form( + key: formKey, + autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/widgets/drawer/avatar.dart b/frontend/pweb/lib/widgets/drawer/avatar.dart new file mode 100644 index 0000000..11dc659 --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/avatar.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:pshared/provider/account.dart'; + + +class AccountAvatar extends StatelessWidget { + const AccountAvatar({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) => UserAccountsDrawerHeader( + accountName: Text(provider.account?.name ?? 'John Doe'), + accountEmail: Text(provider.account?.login ?? 'john.doe@acme.com'), + currentAccountPicture: CircleAvatar( + backgroundImage: (provider.account?.avatarUrl?.isNotEmpty ?? false) + ? CachedNetworkImageProvider(provider.account!.avatarUrl!) + : null, + child: (provider.account?.avatarUrl?.isNotEmpty ?? false) + ? null + : const Icon(Icons.account_circle, size: 50), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/widgets/drawer/tiles/dashboard.dart b/frontend/pweb/lib/widgets/drawer/tiles/dashboard.dart new file mode 100644 index 0000000..07adaed --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/tiles/dashboard.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/app/router/pages.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class DashboardTile extends StatelessWidget { + const DashboardTile({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.dashboard), + title: Text(AppLocalizations.of(context)!.dashboard), + onTap: () => navigate(context, Pages.dashboard), + ); + } +} diff --git a/frontend/pweb/lib/widgets/drawer/tiles/logout.dart b/frontend/pweb/lib/widgets/drawer/tiles/logout.dart new file mode 100644 index 0000000..fd4561a --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/tiles/logout.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/account.dart'; + +import 'package:pweb/app/router/pages.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class LogoutTile extends StatelessWidget { + const LogoutTile({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.logout), + title: Text(AppLocalizations.of(context)!.navigationLogout), + onTap: () => _logout(context), + ); + } + + void _logout(BuildContext context) { + Navigator.pop(context); + final accountProvider = Provider.of(context, listen: false); + accountProvider.logout(); + navigateAndReplace(context, Pages.login); + } +} diff --git a/frontend/pweb/lib/widgets/drawer/tiles/settings/permissions.dart b/frontend/pweb/lib/widgets/drawer/tiles/settings/permissions.dart new file mode 100644 index 0000000..c3dd35f --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/tiles/settings/permissions.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PermissionsSettingsTile extends StatelessWidget { + const PermissionsSettingsTile({ + super.key, + }); + + @override + Widget build(BuildContext context) => ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(AppLocalizations.of(context)!.navigationPermissionsSettings), + onTap: () {// ToDo: account settings + }, + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/drawer/tiles/settings/profile.dart b/frontend/pweb/lib/widgets/drawer/tiles/settings/profile.dart new file mode 100644 index 0000000..d39a2bf --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/tiles/settings/profile.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/app/router/pages.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class ProfileSettingsTile extends StatelessWidget { + const ProfileSettingsTile({ + super.key, + }); + + @override + Widget build(BuildContext context) => ListTile( + leading: const Icon(Icons.settings), + title: Text(AppLocalizations.of(context)!.navigationAccountSettings), + onTap: () => navigateNamed(context, Pages.profile), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/drawer/tiles/settings/roles.dart b/frontend/pweb/lib/widgets/drawer/tiles/settings/roles.dart new file mode 100644 index 0000000..7387516 --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/tiles/settings/roles.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/app/router/pages.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class RolesSettingsTile extends StatelessWidget { + const RolesSettingsTile({ + super.key, + }); + + @override + Widget build(BuildContext context) => ListTile( + leading: const Icon(Icons.manage_accounts), + title: Text(AppLocalizations.of(context)!.navigationRolesSettings), + onTap: () => navigateNamed(context, Pages.roles), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/drawer/tiles/settings/users.dart b/frontend/pweb/lib/widgets/drawer/tiles/settings/users.dart new file mode 100644 index 0000000..7b7f375 --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/tiles/settings/users.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/app/router/pages.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class UsersSettingsTile extends StatelessWidget { + const UsersSettingsTile({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.people), + title: Text(AppLocalizations.of(context)!.navigationUsersSettings), + onTap: () => navigateNamed(context, Pages.users), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/drawer/widget.dart b/frontend/pweb/lib/widgets/drawer/widget.dart new file mode 100644 index 0000000..5649bbc --- /dev/null +++ b/frontend/pweb/lib/widgets/drawer/widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/models/resources.dart'; +import 'package:pshared/provider/permissions.dart'; + +import 'package:pweb/widgets/drawer/avatar.dart'; +import 'package:pweb/widgets/drawer/tiles/dashboard.dart'; +import 'package:pweb/widgets/drawer/tiles/logout.dart'; +import 'package:pweb/widgets/drawer/tiles/settings/profile.dart'; +import 'package:pweb/widgets/drawer/tiles/settings/roles.dart'; +import 'package:pweb/widgets/drawer/tiles/settings/users.dart'; + + +class AppDrawer extends StatelessWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context) => Drawer( + child: Consumer(builder:(context, provider, _) => + ListView( + padding: EdgeInsets.zero, + children: [ + // Shows user avatar / name / email, etc. + const AccountAvatar(), + + const DashboardTile(), + + // Profile & Settings + const Divider(), + if (provider.canAccessResource(ResourceType.accounts)) + const UsersSettingsTile(), + if (provider.canAccessResource(ResourceType.roles)) + const RolesSettingsTile(), + const ProfileSettingsTile(), // always available + + // Logout + const Divider(), + const LogoutTile(), + ], + ), + ), + ); +} diff --git a/frontend/pweb/lib/widgets/employee/avatar/provider.dart b/frontend/pweb/lib/widgets/employee/avatar/provider.dart new file mode 100644 index 0000000..2bcded3 --- /dev/null +++ b/frontend/pweb/lib/widgets/employee/avatar/provider.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/accounts/employees.dart'; + +import 'package:pweb/widgets/employee/avatar/widget.dart'; + + +class EmployeeAvatarProvider extends StatelessWidget { + final String? employeeRef; + final double? radius; + + const EmployeeAvatarProvider({ + super.key, + this.employeeRef, + this.radius, + }); + + @override + Widget build(BuildContext context) => Consumer(builder: (context, provider, _) => EmployeeAvatar( + radius: radius, + avatarUrl: provider.getEmployee(employeeRef)?.avatarUrl, + employeeName: provider.getEmployee(employeeRef)?.name ?? '', + )); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/employee/avatar/widget.dart b/frontend/pweb/lib/widgets/employee/avatar/widget.dart new file mode 100644 index 0000000..a10258e --- /dev/null +++ b/frontend/pweb/lib/widgets/employee/avatar/widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:pshared/utils/name_initials.dart'; + + +class EmployeeAvatar extends StatelessWidget { + final String? avatarUrl; + final String employeeName; + final double? radius; + + const EmployeeAvatar({ + super.key, + this.avatarUrl, + required this.employeeName, + this.radius, + }); + + @override + Widget build(BuildContext context) => CircleAvatar( + radius: radius, + backgroundColor: Theme.of(context).colorScheme.primary.withAlpha(26), + backgroundImage: avatarUrl != null ? CachedNetworkImageProvider(avatarUrl!) : null, + child: avatarUrl == null + ? Text(getNameInitials(employeeName), style: Theme.of(context).textTheme.bodyMedium) + : null, + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/employee/provider.dart b/frontend/pweb/lib/widgets/employee/provider.dart new file mode 100644 index 0000000..2263fcb --- /dev/null +++ b/frontend/pweb/lib/widgets/employee/provider.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/accounts/employees.dart'; + +import 'package:pweb/widgets/employee/tile.dart'; + + +class EmployeeTileProvider extends StatelessWidget { + final String? employeeRef; + final double? avatarRadius; + final Widget? trailing; + + const EmployeeTileProvider({super.key, required this.employeeRef, this.avatarRadius, this.trailing}); + + @override + Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { + if (provider.isLoading) return const Center(child: CircularProgressIndicator()); + return EmployeeTile.fromEmployee( + context: context, + employee: provider.getEmployee(employeeRef), + avatarRadius: avatarRadius, + ); + }); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/employee/tile.dart b/frontend/pweb/lib/widgets/employee/tile.dart new file mode 100644 index 0000000..7ca5c2e --- /dev/null +++ b/frontend/pweb/lib/widgets/employee/tile.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/account/account.dart'; + +import 'package:pweb/widgets/employee/avatar/widget.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class EmployeeTile extends StatelessWidget { + final String name; + final String? avatarUrl; + final double? avatarRadius; + final Widget? trailing; + + const EmployeeTile({super.key, required this.name, this.avatarUrl, this.avatarRadius, this.trailing}); + + factory EmployeeTile.fromEmployee({ + required BuildContext context, + Account? employee, + double? avatarRadius + }) => EmployeeTile( + name: employee?.name ?? AppLocalizations.of(context)!.unknown, + avatarUrl: employee?.avatarUrl, + avatarRadius: avatarRadius, + ); + + @override + Widget build(BuildContext context) => ListTile( + leading: EmployeeAvatar(avatarUrl: avatarUrl, employeeName: name, radius: avatarRadius), + title: Text(name), + trailing: trailing, + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/error/content.dart b/frontend/pweb/lib/widgets/error/content.dart new file mode 100644 index 0000000..2a4ba92 --- /dev/null +++ b/frontend/pweb/lib/widgets/error/content.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/vspacer.dart'; + + +class ErrorSnackBarContent extends StatelessWidget { + final String situation; + final String localizedError; + + const ErrorSnackBarContent({ + super.key, + required this.situation, + required this.localizedError, + }); + + @override + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, // wrap to content + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + situation, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const VSpacer(multiplier: 0.25), + Text(localizedError), + ], + ); +} diff --git a/frontend/pweb/lib/widgets/error/snackbar.dart b/frontend/pweb/lib/widgets/error/snackbar.dart new file mode 100644 index 0000000..9be921e --- /dev/null +++ b/frontend/pweb/lib/widgets/error/snackbar.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:pweb/utils/error_handler.dart'; +import 'package:pweb/widgets/error/content.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +ScaffoldFeatureController notifyUserOfErrorX({ + required ScaffoldMessengerState scaffoldMessenger, + required String errorSituation, + required Object exception, + required AppLocalizations appLocalizations, + int delaySeconds = 3, +}) { + // A. Localized user-friendly error message + final String localizedError = ErrorHandler.handleErrorLocs(appLocalizations, exception); + + // B. Technical details for advanced reference + final String technicalDetails = exception.toString(); + + // C. Build the snack bar + final snackBar = _buildMainErrorSnackBar( + errorSituation: errorSituation, + localizedError: localizedError, + technicalDetails: technicalDetails, + loc: appLocalizations, + scaffoldMessenger: scaffoldMessenger, + delaySeconds: delaySeconds, + ); + + // D. Show it + return scaffoldMessenger.showSnackBar(snackBar); +} + +ScaffoldFeatureController notifyUserOfError({ + required BuildContext context, + required String errorSituation, + required Object exception, + int delaySeconds = 3, +}) => notifyUserOfErrorX( + scaffoldMessenger: ScaffoldMessenger.of(context), + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, +); + +Future executeActionWithNotification({ + required BuildContext context, + required Future Function() action, + required String errorMessage, + int delaySeconds = 3, +}) async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final localizations = AppLocalizations.of(context)!; + + try { + return await action(); + } catch (e) { + // Report the error using your existing notifier. + notifyUserOfErrorX( + scaffoldMessenger: scaffoldMessenger, + errorSituation: errorMessage, + exception: e, + appLocalizations: localizations, + delaySeconds: delaySeconds, + ); + } + return null; +} + +Future> postNotifyUserOfError({ + required ScaffoldMessengerState scaffoldMessenger, + required String errorSituation, + required Object exception, + required AppLocalizations appLocalizations, + int delaySeconds = 3, +}) { + + final completer = Completer>(); + + WidgetsBinding.instance.addPostFrameCallback((_) => completer.complete(notifyUserOfErrorX( + scaffoldMessenger: scaffoldMessenger, + errorSituation: errorSituation, + exception: exception, + appLocalizations: appLocalizations, + delaySeconds: delaySeconds, + )), + ); + + return completer.future; +} + +Future> postNotifyUserOfErrorX({ + required BuildContext context, + required String errorSituation, + required Object exception, + int delaySeconds = 3, +}) => postNotifyUserOfError( + scaffoldMessenger: ScaffoldMessenger.of(context), + errorSituation: errorSituation, + exception: exception, + appLocalizations: AppLocalizations.of(context)!, + delaySeconds: delaySeconds, +); + + +/// 2) A helper function that returns the main SnackBar widget +SnackBar _buildMainErrorSnackBar({ + required String errorSituation, + required String localizedError, + required String technicalDetails, + required AppLocalizations loc, + required ScaffoldMessengerState scaffoldMessenger, + int delaySeconds = 3, +}) => SnackBar( + duration: Duration(seconds: delaySeconds), + content: ErrorSnackBarContent( + situation: errorSituation, + localizedError: localizedError, + ), + action: SnackBarAction( + label: loc.showDetailsAction, + onPressed: () => scaffoldMessenger.showSnackBar(SnackBar( + content: Text(technicalDetails), + duration: const Duration(seconds: 6), + )), + ), +); diff --git a/frontend/pweb/lib/widgets/footer/labels.dart b/frontend/pweb/lib/widgets/footer/labels.dart new file mode 100644 index 0000000..a7ab64c --- /dev/null +++ b/frontend/pweb/lib/widgets/footer/labels.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/footer/policies.dart'; +import 'package:pweb/widgets/footer/support.dart'; +import 'package:pweb/widgets/vspacer.dart'; + + +class FooterLabels extends StatelessWidget { + const FooterLabels({ + super.key, + }); + + @override + Widget build(BuildContext context) => Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SupportLabel(), + const VSpacer(multiplier: 0.25), + const PoliciesLabel(), + ], + ), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/footer/policies.dart b/frontend/pweb/lib/widgets/footer/policies.dart new file mode 100644 index 0000000..815a01b --- /dev/null +++ b/frontend/pweb/lib/widgets/footer/policies.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + +class PoliciesLabel extends StatelessWidget { + const PoliciesLabel({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + final localizations = AppLocalizations.of(context)!; + return Wrap( + spacing: 8, + children: [ + GestureDetector( + onTap: () { + // Navigate to Terms of Service + }, + child: Text( + localizations.footerTermsOfService, + style: theme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ), + const Text('|'), + GestureDetector( + onTap: () { + // Navigate to Privacy Policy + }, + child: Text( + localizations.footerPrivacyPolicy, + style: theme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ), + const Text('|'), + GestureDetector( + onTap: () { + // Navigate to Cookie Policy + }, + child: Text( + localizations.footerCookiePolicy, + style: theme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/footer/support.dart b/frontend/pweb/lib/widgets/footer/support.dart new file mode 100644 index 0000000..711fd96 --- /dev/null +++ b/frontend/pweb/lib/widgets/footer/support.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/hspacer.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SupportLabel extends StatelessWidget { + const SupportLabel({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + final localizations = AppLocalizations.of(context)!; + return Row( + children: [ + Row( + children: [ + Text( + '${localizations.footerSupport}: ', + style: theme.labelSmall, + ), + GestureDetector( + onTap: () { + // Add your email handling logic here + }, + child: Text( + localizations.footerEmail, // Localized email + style: theme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + ), + const HSpacer(multiplier: 0.25), + const Text('|'), + const HSpacer(multiplier: 0.25), + Text( + '${localizations.footerPhoneLabel}: ${localizations.footerPhone}', // Localized phone + style: theme.labelSmall, + ), + ], + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/footer/widget.dart b/frontend/pweb/lib/widgets/footer/widget.dart new file mode 100644 index 0000000..ebb4866 --- /dev/null +++ b/frontend/pweb/lib/widgets/footer/widget.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/footer/labels.dart'; +import 'package:pweb/widgets/logo.dart'; +import 'package:pweb/widgets/hspacer.dart'; + + +class FooterWidget extends StatelessWidget { + const FooterWidget({super.key}); + + @override + Widget build(BuildContext context) => ClipRect( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const ServiceLogo(), + const HSpacer(), + const FooterLabels(), + ], + ), + ), + ); +} + + diff --git a/frontend/pweb/lib/widgets/hspacer.dart b/frontend/pweb/lib/widgets/hspacer.dart new file mode 100644 index 0000000..aeeeeb8 --- /dev/null +++ b/frontend/pweb/lib/widgets/hspacer.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + + +class HSpacer extends StatelessWidget{ + final double spacing; + final double multiplier; + + const HSpacer({super.key, this.spacing = 16, this.multiplier = 1.0}); + + @override + Widget build(BuildContext context) { + return SizedBox(width: spacing * multiplier); + } +} diff --git a/frontend/pweb/lib/widgets/logo.dart b/frontend/pweb/lib/widgets/logo.dart new file mode 100644 index 0000000..feca497 --- /dev/null +++ b/frontend/pweb/lib/widgets/logo.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + + +class ServiceLogo extends StatelessWidget { + final double size; + + const ServiceLogo({ super.key, this.size = 48 }); + + @override + Widget build(BuildContext context) => SizedBox( + height: size, + width: size, + child: Image.asset('resources/logo.png'), + ); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/password/hint/error.dart b/frontend/pweb/lib/widgets/password/hint/error.dart new file mode 100644 index 0000000..25c7284 --- /dev/null +++ b/frontend/pweb/lib/widgets/password/hint/error.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/password/hint/widget.dart'; + + +class PasswordValidationErrorLabel extends StatelessWidget { + final String labelText; + const PasswordValidationErrorLabel({super.key, required this.labelText}); + + @override + Widget build(BuildContext context) { + return PasswordValidationOutput( + children: [ + Text( + labelText, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.error, + ) + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/password/hint/full.dart b/frontend/pweb/lib/widgets/password/hint/full.dart new file mode 100644 index 0000000..232f2f7 --- /dev/null +++ b/frontend/pweb/lib/widgets/password/hint/full.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import 'package:fancy_password_field/fancy_password_field.dart'; + +import 'package:pweb/widgets/password/hint/validation_result.dart'; +import 'package:pweb/widgets/password/hint/widget.dart'; + + +Widget expandedValidation(BuildContext context, Set rules, String value) { + return PasswordValidationOutput( + children: rules.map( + (rule) => PasswordValidationResult( + ruleName: rule.name, + result: rule.validate(value), + ), + ).toList() + ); +} diff --git a/frontend/pweb/lib/widgets/password/hint/short.dart b/frontend/pweb/lib/widgets/password/hint/short.dart new file mode 100644 index 0000000..43273b3 --- /dev/null +++ b/frontend/pweb/lib/widgets/password/hint/short.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import 'package:fancy_password_field/fancy_password_field.dart'; + +import 'package:pweb/widgets/password/hint/error.dart'; +import 'package:pweb/widgets/password/hint/widget.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +Widget shortValidation(BuildContext context, Set rules, String value) { + if (value.isEmpty) return Container(); + final failedRules = rules.where((rule) => !rule.validate(value)); + return (failedRules.isNotEmpty) + ? PasswordValidationOutput( + children: [ + PasswordValidationErrorLabel( + labelText: AppLocalizations.of(context)!.passwordValidationError( + rules.firstWhere((rule) => !rule.validate(value)).name + ), + ), + ], + ) + : Container(); +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/password/hint/validation_result.dart b/frontend/pweb/lib/widgets/password/hint/validation_result.dart new file mode 100644 index 0000000..ee967bf --- /dev/null +++ b/frontend/pweb/lib/widgets/password/hint/validation_result.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + + +class PasswordValidationResult extends StatelessWidget { + final String ruleName; + final bool result; + + const PasswordValidationResult({ + super.key, + required this.ruleName, + required this.result + }); + + Color _selectColor(BuildContext context, bool res) { + final scheme = Theme.of(context).colorScheme; + return res ? scheme.secondary : scheme.error; + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + result ? Icons.check : Icons.close, + color: _selectColor(context, result), + ), + const SizedBox(width: 8), + Text( + ruleName, + style: TextStyle(color: _selectColor(context, result)), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/password/hint/widget.dart b/frontend/pweb/lib/widgets/password/hint/widget.dart new file mode 100644 index 0000000..473f570 --- /dev/null +++ b/frontend/pweb/lib/widgets/password/hint/widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/vspacer.dart'; + + +class PasswordValidationOutput extends StatelessWidget { + final List children; + + const PasswordValidationOutput({super.key, required this.children}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + VSpacer(multiplier: 0.25), + ListView( + shrinkWrap: true, + children: children, + ) + ] + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/password/password.dart b/frontend/pweb/lib/widgets/password/password.dart new file mode 100644 index 0000000..318e957 --- /dev/null +++ b/frontend/pweb/lib/widgets/password/password.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import 'package:fancy_password_field/fancy_password_field.dart'; + +import 'package:pweb/config/constants.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PasswordField extends StatefulWidget { + final TextEditingController controller; + final ValueChanged? onValid; + final bool hasStrengthIndicator; + final String? labelText; + final Set rules; + final AutovalidateMode? autovalidateMode; + final Widget Function(Set, String)? validationRuleBuilder; + + const PasswordField({ + super.key, + required this.controller, + this.onValid, + this.validationRuleBuilder, + this.labelText, + this.hasStrengthIndicator = false, + this.autovalidateMode, + this.rules = const {}, + }); + + @override + State createState() => _PasswordFieldState(); +} + +class _PasswordFieldState extends State { + bool _lastValidationResult = false; + + void _onChanged(String value) { + bool isValid = widget.rules.every((rule) => rule.validate(value)); + + // Only trigger onValid if validation result has changed + if (isValid != _lastValidationResult) { + _lastValidationResult = isValid; + widget.onValid?.call(isValid); + } + } + + @override + Widget build(BuildContext context) { + return FancyPasswordField( + key: widget.key, + controller: widget.controller, + decoration: InputDecoration( + labelText: widget.labelText ?? AppLocalizations.of(context)!.password, + ), + validationRules: widget.rules, + hasStrengthIndicator: widget.hasStrengthIndicator, + validationRuleBuilder: widget.validationRuleBuilder, + autovalidateMode: widget.autovalidateMode, + onChanged: _onChanged, + ); + } +} + +Widget defaulRulesPasswordField( + BuildContext context, { + required TextEditingController controller, + Key? key, + ValueChanged? onValid, + Widget Function(Set, String)? validationRuleBuilder, + String? labelText, + FocusNode? focusNode, + AutovalidateMode? autovalidateMode, + bool hasStrengthIndicator = false, + Set additionalRules = const {}, +}) { + Set rules = { + DigitValidationRule( + customText: AppLocalizations.of(context)!.passwordValidationRuleDigit, + ), + UppercaseValidationRule( + customText: AppLocalizations.of(context)!.passwordValidationRuleUpperCase, + ), + LowercaseValidationRule( + customText: AppLocalizations.of(context)!.passwordValidationRuleLowerCase, + ), + MinCharactersValidationRule( + Constants.minPasswordCharacters, + customText: AppLocalizations.of(context)! + .passwordValidationRuleMinCharacters(Constants.minPasswordCharacters), + ), + ...additionalRules, + }; + + return PasswordField( + key: key, + controller: controller, + onValid: onValid, + validationRuleBuilder: validationRuleBuilder, + hasStrengthIndicator: hasStrengthIndicator, + labelText: labelText, + autovalidateMode: autovalidateMode, + rules: rules, + ); +} diff --git a/frontend/pweb/lib/widgets/password/verify.dart b/frontend/pweb/lib/widgets/password/verify.dart new file mode 100644 index 0000000..8507821 --- /dev/null +++ b/frontend/pweb/lib/widgets/password/verify.dart @@ -0,0 +1,95 @@ +import 'package:flutter/widgets.dart'; + +import 'package:fancy_password_field/fancy_password_field.dart'; + +import 'package:pweb/widgets/password/hint/error.dart'; +import 'package:pweb/widgets/password/hint/short.dart'; +import 'package:pweb/widgets/password/password.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PasswordVeirificationRule extends ValidationRule { + final String ruleName; + final TextEditingController externalPasswordController; + + PasswordVeirificationRule({ + required this.ruleName, + required this.externalPasswordController, + }); + + @override + String get name => ruleName; + + @override + bool get showName => true; + + @override + bool validate(String value) => value == externalPasswordController.text; +} + +class VerifyPasswordField extends StatefulWidget { + final ValueChanged? onValid; + final TextEditingController controller; + final TextEditingController externalPasswordController; + + const VerifyPasswordField({ + super.key, + this.onValid, + required this.controller, + required this.externalPasswordController, + }); + + @override + State createState() => _VerifyPasswordFieldState(); +} + +class _VerifyPasswordFieldState extends State { + bool _isCurrentlyValid = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_validatePassword); + widget.externalPasswordController.addListener(_validatePassword); + } + + void _validatePassword() { + final isValid = widget.controller.text == widget.externalPasswordController.text; + + // Only call onValid if the validity state has changed to prevent infinite loops + if (isValid != _isCurrentlyValid) { + setState(() { + _isCurrentlyValid = isValid; + }); + widget.onValid?.call(isValid); + } + } + + @override + Widget build(BuildContext context) { + final rule = PasswordVeirificationRule( + ruleName: AppLocalizations.of(context)!.passwordsDoNotMatch, + externalPasswordController: widget.externalPasswordController, + ); + + return defaulRulesPasswordField( + context, + controller: widget.controller, + key: widget.key, + labelText: AppLocalizations.of(context)!.confirmPassword, + additionalRules: { rule }, + validationRuleBuilder: (rules, value) => rule.validate(value) + ? shortValidation(context, rules, value) + : PasswordValidationErrorLabel(labelText: AppLocalizations.of(context)!.passwordsDoNotMatch), + onValid: widget.onValid, + ); + } + + @override + void dispose() { + widget.controller.removeListener(_validatePassword); + widget.externalPasswordController.removeListener(_validatePassword); + super.dispose(); + } +} diff --git a/frontend/pweb/lib/widgets/protected/widget.dart b/frontend/pweb/lib/widgets/protected/widget.dart new file mode 100644 index 0000000..49ce816 --- /dev/null +++ b/frontend/pweb/lib/widgets/protected/widget.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/models/resources.dart'; +import 'package:pshared/models/permissions/action.dart' as perm; +import 'package:pshared/provider/permissions.dart'; + + +T? protectedWidgetctx(BuildContext context, ResourceType resource, T child, {perm.Action? action}) { + return protectedWidget(Provider.of(context, listen: false), resource, child, action: action); +} + +T? protectedWidget(PermissionsProvider provider, ResourceType resource, T child, {perm.Action? action}) { + return provider.canAccessResource(resource, action: action) ? child : null; +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/search.dart b/frontend/pweb/lib/widgets/search.dart new file mode 100644 index 0000000..d65a945 --- /dev/null +++ b/frontend/pweb/lib/widgets/search.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + + +class SearchBox extends StatelessWidget { + final TextEditingController controller; + final String hintText; + final ValueChanged onChanged; + final VoidCallback? onClear; + final String? Function(String?)? validator; + final String? labelText; + final String? helperText; + + const SearchBox({ + super.key, + required this.controller, + required this.hintText, + required this.onChanged, + this.onClear, + this.validator, + this.labelText, + this.helperText, + }); + + @override + Widget build(BuildContext context) => TextFormField( + controller: controller, + onChanged: onChanged, + validator: validator, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + suffixIcon: ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) => value.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.clear(); + if (onClear != null) { + onClear!(); + } + onChanged(''); + }, + ) + : const SizedBox.shrink(), + ), + hintText: hintText, + labelText: labelText, + helperText: helperText, + border: const UnderlineInputBorder(), + ), + ); +} diff --git a/frontend/pweb/lib/widgets/sidebar/destinations.dart b/frontend/pweb/lib/widgets/sidebar/destinations.dart new file mode 100644 index 0000000..62598c9 --- /dev/null +++ b/frontend/pweb/lib/widgets/sidebar/destinations.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +enum PayoutDestination { + dashboard(Icons.dashboard_outlined, 'dashboard'), + sendPayout(Icons.send_outlined, 'sendPayout'), + recipients(Icons.people_outline, 'recipients'), + reports(Icons.insert_chart, 'reports'), + settings(Icons.settings_outlined, 'settings'), + methods(Icons.credit_card, 'methods'), + payment(Icons.payment, 'payout'), + addrecipient(Icons.app_registration, 'add recipient'), + editwallet(Icons.wallet, 'edit wallet'); + + + const PayoutDestination(this.icon, this.labelKey); + + final IconData icon; + final String labelKey; + + String localizedLabel(BuildContext context) { + final loc = AppLocalizations.of(context)!; + switch (this) { + case PayoutDestination.dashboard: + return loc.payoutNavDashboard; + case PayoutDestination.sendPayout: + return loc.payoutNavSendPayout; + case PayoutDestination.recipients: + return loc.payoutNavRecipients; + case PayoutDestination.reports: + return loc.payoutNavReports; + case PayoutDestination.settings: + return loc.payoutNavSettings; + case PayoutDestination.methods: + return loc.payoutNavMethods; + case PayoutDestination.payment: + return loc.payout; + case PayoutDestination.addrecipient: + return loc.addRecipient; + case PayoutDestination.editwallet: + return 'Edit Wallet'; + } + } +} diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart new file mode 100644 index 0000000..68c8d99 --- /dev/null +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/pages/address_book/form/page.dart'; +import 'package:pweb/pages/address_book/page/page.dart'; +import 'package:pweb/pages/payment_methods/page.dart'; +import 'package:pweb/pages/payment_page/page.dart'; +import 'package:pweb/pages/payment_page/wallet/edit/page.dart'; +import 'package:pweb/pages/report/page.dart'; +import 'package:pweb/pages/settings/profile/page.dart'; +import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/widgets/appbar/app_bar.dart'; +import 'package:pweb/pages/dashboard/dashboard.dart'; +import 'package:pweb/widgets/sidebar/destinations.dart'; +import 'package:pweb/widgets/sidebar/sidebar.dart'; + + +class PageSelector extends StatelessWidget { + const PageSelector({super.key}); + + @override + Widget build(BuildContext context) { + final provider = context.watch(); + + Widget content; + switch (provider.selected) { + case PayoutDestination.dashboard: + content = DashboardPage( + onRecipientSelected: (recipient) => + provider.selectRecipient(recipient), + onGoToPaymentWithoutRecipient: provider.startPaymentWithoutRecipient, + ); + break; + + case PayoutDestination.recipients: + content = RecipientAddressBookPage( + onRecipientSelected: (recipient) => + provider.selectRecipient(recipient, fromList: true), + onAddRecipient: provider.goToAddRecipient, + onEditRecipient: provider.editRecipient, + ); + break; + + case PayoutDestination.addrecipient: + final recipient = provider.recipientProvider?.selectedRecipient; + content = AdressBookRecipientForm( + recipient: recipient, + onSaved: (_) => provider.selectPage(PayoutDestination.recipients), + ); + break; + + case PayoutDestination.payment: + content = PaymentPage( + type: provider.type, + onBack: (_) => provider.goBackFromPayment(), + ); + break; + + case PayoutDestination.settings: + content = ProfileSettingsPage(); + break; + + case PayoutDestination.reports: + content = OperationHistoryPage(); + break; + + case PayoutDestination.methods: + content = PaymentConfigPage( + onWalletTap: provider.selectWallet, + ); + break; + + case PayoutDestination.editwallet: + final wallet = provider.walletsProvider?.selectedWallet; + content = wallet != null + ? WalletEditPage( + wallet: wallet, + onBack: () => provider.goBackFromPayment(), + ) + : const Center(child: Text('No wallet selected')); //TODO Localize + break; + + default: + content = Text(provider.selected.name); + } + + return Scaffold( + appBar: PayoutAppBar( + title: Text(provider.selected.localizedLabel(context)), + onAddFundsPressed: () {}, + onLogout: () => debugPrint('Logout clicked'), + ), + body: Padding( + padding: const EdgeInsets.only(left: 200, top: 40, right: 200), + child: Row( + spacing: 40, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PayoutSidebar( + selected: provider.selected, + onSelected: provider.selectPage, + onLogout: () => debugPrint('Logout clicked'), + ), + Expanded(child: content), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/sidebar/side_menu.dart b/frontend/pweb/lib/widgets/sidebar/side_menu.dart new file mode 100644 index 0000000..494ffdb --- /dev/null +++ b/frontend/pweb/lib/widgets/sidebar/side_menu.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:pweb/services/amplitude.dart'; + +import 'package:pweb/widgets/sidebar/destinations.dart'; + + +class SideMenuColumn extends StatelessWidget { + final ThemeData theme; + final String? avatarUrl; + final String? userName; + final List items; + final PayoutDestination selected; + final void Function(PayoutDestination) onSelected; + + const SideMenuColumn({ + super.key, + required this.theme, + required this.avatarUrl, + required this.userName, + required this.items, + required this.selected, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(16), + color: theme.colorScheme.onSecondary, + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 8), + children: items.map((item) { + final isSelected = item == selected; + final backgroundColor = isSelected + ? theme.colorScheme.primaryContainer + : Colors.transparent; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: InkWell( + onTap: () { + onSelected(item); + AmplitudeService.pageOpened(item, uiSource: 'sidebar'); + }, + borderRadius: BorderRadius.circular(12), + hoverColor: theme.colorScheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 12), + child: Row( + children: [ + Icon(item.icon, color: theme.iconTheme.color, size: 28), + const SizedBox(width: 16), + Text( + item.localizedLabel(context), + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/sidebar/sidebar.dart b/frontend/pweb/lib/widgets/sidebar/sidebar.dart new file mode 100644 index 0000000..d3dac81 --- /dev/null +++ b/frontend/pweb/lib/widgets/sidebar/sidebar.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/sidebar/destinations.dart'; +import 'package:pweb/widgets/sidebar/side_menu.dart'; +import 'package:pweb/widgets/sidebar/user.dart'; + + +class PayoutSidebar extends StatelessWidget { + const PayoutSidebar({ + super.key, + required this.selected, + required this.onSelected, + this.onLogout, + this.userName, + this.avatarUrl, + }); + + final PayoutDestination selected; + final ValueChanged onSelected; + final VoidCallback? onLogout; + + final String? userName; + final String? avatarUrl; + + + @override + Widget build(BuildContext context) { + final items = [ + PayoutDestination.dashboard, + PayoutDestination.recipients, + PayoutDestination.methods, + PayoutDestination.reports, + ]; + + final theme = Theme.of(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + UserProfileCard( + theme: theme, + avatarUrl: avatarUrl, + userName: userName, + selected: selected, + onSelected: onSelected + ), + const SizedBox(height: 8), + SideMenuColumn( + theme: theme, + avatarUrl: avatarUrl, + userName: userName, + items: items, + selected: selected, + onSelected: onSelected, + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/sidebar/user.dart b/frontend/pweb/lib/widgets/sidebar/user.dart new file mode 100644 index 0000000..a979614 --- /dev/null +++ b/frontend/pweb/lib/widgets/sidebar/user.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/sidebar/destinations.dart'; + + +class UserProfileCard extends StatelessWidget { + final ThemeData theme; + final String? avatarUrl; + final String? userName; + final PayoutDestination selected; + final void Function(PayoutDestination) onSelected; + + const UserProfileCard({ + super.key, + required this.theme, + required this.avatarUrl, + required this.userName, + required this.selected, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + bool isSelected = selected == PayoutDestination.settings; + final backgroundColor = isSelected + ? theme.colorScheme.primaryContainer + : Colors.transparent; + + return Material( + elevation: 4, + borderRadius: BorderRadius.circular(14), + color: theme.colorScheme.onSecondary, + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () => onSelected(PayoutDestination.settings), + child: Container( + height: 80, + width: 320, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(14), + ), + padding: const EdgeInsets.only(top: 15.0, left: 30, right: 20, bottom: 15), + child: Row( + spacing: 5, + children: [ + CircleAvatar( + radius: 20, + foregroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, + child: avatarUrl == null ? const Icon(Icons.person, size: 28) : null, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + userName ?? 'User Name', + style: theme.textTheme.bodyLarge?.copyWith( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/stats/card.dart b/frontend/pweb/lib/widgets/stats/card.dart new file mode 100644 index 0000000..50f7daf --- /dev/null +++ b/frontend/pweb/lib/widgets/stats/card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/vspacer.dart'; + + +class StatCard extends StatelessWidget { + final IconData icon; + final String text; + final int count; + final Color color; + + const StatCard({ + super.key, + required this.icon, + required this.text, + required this.count, + required this.color, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Expanded( + child: Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Icon(icon, size: 24, color: color), + const VSpacer(multiplier: 0.25), + Text( + '$count', + style: theme.textTheme.titleMedium, + ), + Text( + text, + style: theme.textTheme.bodySmall, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/widgets/text_field.dart b/frontend/pweb/lib/widgets/text_field.dart new file mode 100644 index 0000000..5cdd542 --- /dev/null +++ b/frontend/pweb/lib/widgets/text_field.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + + +class NotEmptyTextFormField extends StatelessWidget { + final String labelText; + final String error; + final TextEditingController controller; + final ValueChanged? onValid; + final String? hintText; + final bool readOnly; + + const NotEmptyTextFormField({ + super.key, + required this.controller, + required this.labelText, + required this.error, + this.onValid, + this.hintText, + required this.readOnly, + }); + + bool _validate(String? value) { + return !(value == null || value.isNotEmpty); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + decoration: InputDecoration(labelText: labelText, hintText: hintText), + validator: (value) => _validate(value) ? error : null, + onChanged: (value) { + if (onValid != null) onValid!(_validate(value)); + }, + readOnly: readOnly, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/widgets/username.dart b/frontend/pweb/lib/widgets/username.dart new file mode 100644 index 0000000..e9452c8 --- /dev/null +++ b/frontend/pweb/lib/widgets/username.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class UsernameField extends StatelessWidget { + final TextEditingController controller; + final ValueChanged? onValid; + + const UsernameField({ + super.key, + required this.controller, + this.onValid, + }); + + String? _reportResult(String? msg) { + onValid?.call(msg == null); + return msg; + } + + @override + Widget build(BuildContext context) => TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.username, + hintText: AppLocalizations.of(context)!.usernameHint, + ), + validator: (value) { + return _reportResult((value?.isNotEmpty ?? false) ? null : AppLocalizations.of(context)!.usernameErrorInvalid); + // bool isValid = value != null && EmailValidator.validate(value); + // if (!isValid) { + // return _reportResult(AppLocalizations.of(context)!.usernameErrorInvalid); + // } + // final tld = value.split('.').last; + // isValid = tlds.contains(tld); + // if (!isValid) { + // return _reportResult(AppLocalizations.of(context)!.usernameUnknownTLD(tld)); + // } + // return _reportResult(null); + }, + onChanged: (value) => onValid?.call(value.isNotEmpty), + ); +} diff --git a/frontend/pweb/lib/widgets/vspacer.dart b/frontend/pweb/lib/widgets/vspacer.dart new file mode 100644 index 0000000..99db732 --- /dev/null +++ b/frontend/pweb/lib/widgets/vspacer.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + + +class VSpacer extends StatelessWidget{ + final double spacing; + final double multiplier; + + const VSpacer({super.key, this.spacing = 16, this.multiplier = 1.0}); + + @override + Widget build(BuildContext context) { + return SizedBox(height: spacing * multiplier); + } +} diff --git a/frontend/pweb/linux/.gitignore b/frontend/pweb/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/frontend/pweb/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/frontend/pweb/linux/CMakeLists.txt b/frontend/pweb/linux/CMakeLists.txt new file mode 100644 index 0000000..d447c09 --- /dev/null +++ b/frontend/pweb/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "web") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.web") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/frontend/pweb/linux/flutter/CMakeLists.txt b/frontend/pweb/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/frontend/pweb/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/frontend/pweb/linux/flutter/generated_plugin_registrant.cc b/frontend/pweb/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..13807bb --- /dev/null +++ b/frontend/pweb/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_timezone_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin"); + flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/frontend/pweb/linux/flutter/generated_plugin_registrant.h b/frontend/pweb/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/frontend/pweb/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend/pweb/linux/flutter/generated_plugins.cmake b/frontend/pweb/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..b01d1fd --- /dev/null +++ b/frontend/pweb/linux/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + flutter_timezone + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend/pweb/linux/runner/CMakeLists.txt b/frontend/pweb/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/frontend/pweb/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/frontend/pweb/linux/runner/main.cc b/frontend/pweb/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/frontend/pweb/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/frontend/pweb/linux/runner/my_application.cc b/frontend/pweb/linux/runner/my_application.cc new file mode 100644 index 0000000..dcd3802 --- /dev/null +++ b/frontend/pweb/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "web"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "web"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/frontend/pweb/linux/runner/my_application.h b/frontend/pweb/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/frontend/pweb/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/frontend/pweb/macos/.gitignore b/frontend/pweb/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/frontend/pweb/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/frontend/pweb/macos/Flutter/Flutter-Debug.xcconfig b/frontend/pweb/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/frontend/pweb/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/pweb/macos/Flutter/Flutter-Release.xcconfig b/frontend/pweb/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/frontend/pweb/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..79f5652 --- /dev/null +++ b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import amplitude_flutter +import file_selector_macos +import flutter_timezone +import path_provider_foundation +import share_plus +import shared_preferences_foundation +import sqflite_darwin +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/frontend/pweb/macos/Podfile b/frontend/pweb/macos/Podfile new file mode 100644 index 0000000..29c8eb3 --- /dev/null +++ b/frontend/pweb/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/frontend/pweb/macos/Runner.xcodeproj/project.pbxproj b/frontend/pweb/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a1153bb --- /dev/null +++ b/frontend/pweb/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* web.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "web.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* web.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* web.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.web.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/web.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/web"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.web.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/web.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/web"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.web.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/web.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/web"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/frontend/pweb/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/pweb/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend/pweb/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/pweb/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/pweb/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..13ff3eb --- /dev/null +++ b/frontend/pweb/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/pweb/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/pweb/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/frontend/pweb/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/pweb/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/pweb/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/frontend/pweb/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/pweb/macos/Runner/AppDelegate.swift b/frontend/pweb/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/frontend/pweb/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend/pweb/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/pweb/macos/Runner/Configs/AppInfo.xcconfig b/frontend/pweb/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..721d293 --- /dev/null +++ b/frontend/pweb/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = web + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.web + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/frontend/pweb/macos/Runner/Configs/Debug.xcconfig b/frontend/pweb/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/frontend/pweb/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend/pweb/macos/Runner/Configs/Release.xcconfig b/frontend/pweb/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/frontend/pweb/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/frontend/pweb/macos/Runner/Configs/Warnings.xcconfig b/frontend/pweb/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/frontend/pweb/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/frontend/pweb/macos/Runner/DebugProfile.entitlements b/frontend/pweb/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/frontend/pweb/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/frontend/pweb/macos/Runner/Info.plist b/frontend/pweb/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/frontend/pweb/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/frontend/pweb/macos/Runner/MainFlutterWindow.swift b/frontend/pweb/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/frontend/pweb/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/frontend/pweb/macos/Runner/Release.entitlements b/frontend/pweb/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/frontend/pweb/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/frontend/pweb/macos/RunnerTests/RunnerTests.swift b/frontend/pweb/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/frontend/pweb/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/frontend/pweb/pubspec.lock b/frontend/pweb/pubspec.lock new file mode 100644 index 0000000..11a25c7 --- /dev/null +++ b/frontend/pweb/pubspec.lock @@ -0,0 +1,1374 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a + url: "https://pub.dev" + source: hosted + version: "88.0.0" + amplitude_flutter: + dependency: "direct main" + description: + name: amplitude_flutter + sha256: af506e2e326251be89eee9bef5a86b10e2b442c3e52e1305e39a67e55d6d0f74 + url: "https://pub.dev" + source: hosted + version: "4.3.7" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" + url: "https://pub.dev" + source: hosted + version: "8.1.1" + appflowy_board: + dependency: "direct main" + description: + name: appflowy_board + sha256: "4dc5ce013913723ca330db350df154abdf1315285bcf61a35d65471e9ea00517" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + badges: + dependency: "direct main" + description: + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + base58check: + dependency: transitive + description: + name: base58check + sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + bech32: + dependency: transitive + description: + name: bech32 + sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "6439a9c71a4e6eca8d9490c1b380a25b02675aa688137dfbe66d2062884a23ac" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "2b21a125d66a86b9511cc3fb6c668c42e9a1185083922bf60e46d483a81a9712" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: fd3c09f4bbff7fa6e8d8ef688a0b2e8a6384e6483a25af0dac75fef362bcfe6f + url: "https://pub.dev" + source: hosted + version: "2.7.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: ab27e46c8aa233e610cf6084ee6d8a22c6f873a0a9929241d8855b7a72978ae7 + url: "https://pub.dev" + source: hosted + version: "9.3.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb + url: "https://pub.dev" + source: hosted + version: "8.11.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + country_flags: + dependency: "direct main" + description: + name: country_flags + sha256: "78a7bf8aabd7ae1a90087f0c517471ac9ebfe07addc652692f58da0f0f833196" + url: "https://pub.dev" + source: hosted + version: "3.3.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + dotted_border: + dependency: "direct main" + description: + name: dotted_border + sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + email_validator: + dependency: "direct main" + description: + name: email_validator + sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb + url: "https://pub.dev" + source: hosted + version: "3.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fancy_password_field: + dependency: "direct main" + description: + name: fancy_password_field + sha256: ff2bd9daecfc09d00c978657642774d11320020678e589bdb5469b5079385448 + url: "https://pub.dev" + source: hosted + version: "2.0.8" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" + url: "https://pub.dev" + source: hosted + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" + url: "https://pub.dev" + source: hosted + version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_multi_formatter: + dependency: "direct main" + description: + name: flutter_multi_formatter + sha256: "29d9b3d30a985f5a9c3dd52b4e25e64b9a20ebdcf4d9fed0c71e653406598604" + url: "https://pub.dev" + source: hosted + version: "2.13.10" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 + url: "https://pub.dev" + source: hosted + version: "2.0.30" + flutter_settings_ui: + dependency: "direct main" + description: + name: flutter_settings_ui + sha256: dcc506fab724192594e5c232b6214a941abd6e7b5151626635b89258fadbc17c + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_timezone: + dependency: "direct main" + description: + name: flutter_timezone + sha256: "13b2109ad75651faced4831bf262e32559e44aa549426eab8a597610d385d934" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + font_awesome_flutter: + dependency: transitive + description: + name: font_awesome_flutter + sha256: "27af5982e6c510dec1ba038eff634fa284676ee84e3fd807225c80c4ad869177" + url: "https://pub.dev" + source: hosted + version: "10.10.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: ced3fdc143c1437234ac3b8e985f3286cf138968bb83ca9a6f94d22f2951c6b9 + url: "https://pub.dev" + source: hosted + version: "16.2.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + icann_tlds: + dependency: "direct main" + description: + name: icann_tlds + sha256: "399432af3f1882780bfe57ade60657367f244cb74efd91cd426ffda6644e967e" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" + url: "https://pub.dev" + source: hosted + version: "0.8.13+1" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + jovial_misc: + dependency: transitive + description: + name: jovial_misc + sha256: "4301011027d87b8b919cb862db84071a34448eadbb32cc8d40fe505424dfe69a" + url: "https://pub.dev" + source: hosted + version: "0.9.2" + jovial_svg: + dependency: "direct main" + description: + name: jovial_svg + sha256: "6791b1435547bdc0793081a166d41a8a313ebc61e4e5136fb7a3218781fb9e50" + url: "https://pub.dev" + source: hosted + version: "1.1.27" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + markdown_widget: + dependency: "direct main" + description: + name: markdown_widget + sha256: b52c13d3ee4d0e60c812e15b0593f142a3b8a2003cde1babb271d001a1dbdc1c + url: "https://pub.dev" + source: hosted + version: "2.3.2+8" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + password_strength: + dependency: transitive + description: + name: password_strength + sha256: "0e51e3d864e37873a1347e658147f88b66e141ee36c58e19828dc5637961e1ce" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.dev" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + pin_code_fields: + dependency: "direct main" + description: + name: pin_code_fields + sha256: "4c0db7fbc889e622e7c71ea54b9ee624bb70c7365b532abea0271b17ea75b729" + url: "https://pub.dev" + source: hosted + version: "8.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pshared: + dependency: "direct main" + description: + path: "../pshared" + relative: true + source: path + version: "1.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 + url: "https://pub.dev" + source: hosted + version: "11.1.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + syncfusion_flutter_charts: + dependency: "direct main" + description: + name: syncfusion_flutter_charts + sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8" + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0 + url: "https://pub.dev" + source: hosted + version: "30.2.7" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + timeago: + dependency: "direct main" + description: + name: timeago + sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e + url: "https://pub.dev" + source: hosted + version: "3.7.1" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" + url: "https://pub.dev" + source: hosted + version: "6.3.18" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + web: + dependency: "direct main" + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/frontend/pweb/pubspec.yaml b/frontend/pweb/pubspec.yaml new file mode 100644 index 0000000..b8bc11c --- /dev/null +++ b/frontend/pweb/pubspec.yaml @@ -0,0 +1,140 @@ +name: pweb +description: "Profee Pay B2B Web Client" +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 3.2.936+14 + +environment: + sdk: ^3.8.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + amplitude_flutter: ^4.0.1 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + pshared: + path: ../pshared + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 + intl: ^0.20.2 + provider: ^6.1.5 + shared_preferences: ^2.2.3 + flutter_launcher_icons: ^0.14.0 + country_flags: ^3.0.0 + logging: ^1.2.0 + email_validator: ^3.0.0 + fancy_password_field: ^2.0.7 + web: ^1.1.0 + share_plus: ^11.0.0 + collection: ^1.18.0 + icann_tlds: ^1.0.0 + flutter_timezone: ^4.0.0 + json_annotation: ^4.9.0 + go_router: ^16.0.0 + jovial_svg: ^1.1.23 + cached_network_image: ^3.4.1 + image_picker: ^1.1.2 + appflowy_board: ^0.1.2 + badges: ^3.1.2 + markdown_widget: ^2.3.2+6 + timeago: ^3.7.0 + flutter_settings_ui: ^3.0.1 + pin_code_fields: ^8.0.1 + fl_chart: ^1.0.0 + syncfusion_flutter_charts: ^30.1.40 + flutter_multi_formatter: ^2.13.7 + dotted_border: ^3.1.0 + + + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.11 + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + generate: true + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - resources/logo.png + - resources/logo.si + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package + +flutter_intl: + enabled: true + localizations_delegates: + - flutter_localizations + - app_localizations + +flutter_launcher_icons: + image_path: "resources/logo.png" + android: true + ios: true + web: + generate: true + image_path: "resources/logo.png" \ No newline at end of file diff --git a/frontend/pweb/resources/logo.png b/frontend/pweb/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7172675e545585c4d8a18566f210127aa68e94a7 GIT binary patch literal 34400 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 literal 0 HcmV?d00001 diff --git a/frontend/pweb/web/icons/Icon-maskable-192.png b/frontend/pweb/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..9d7cfe6597771fd7e99827897c63cbce37dea4f6 GIT binary patch literal 7074 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 literal 0 HcmV?d00001 diff --git a/frontend/pweb/web/index.html b/frontend/pweb/web/index.html new file mode 100644 index 0000000..475a0a7 --- /dev/null +++ b/frontend/pweb/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + web + + + + + + diff --git a/frontend/pweb/web/manifest.json b/frontend/pweb/web/manifest.json new file mode 100644 index 0000000..e5b4f87 --- /dev/null +++ b/frontend/pweb/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "web", + "short_name": "web", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} \ No newline at end of file diff --git a/frontend/pweb/windows/.gitignore b/frontend/pweb/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/frontend/pweb/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/frontend/pweb/windows/CMakeLists.txt b/frontend/pweb/windows/CMakeLists.txt new file mode 100644 index 0000000..0547238 --- /dev/null +++ b/frontend/pweb/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(web LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "web") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/frontend/pweb/windows/flutter/CMakeLists.txt b/frontend/pweb/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/frontend/pweb/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/frontend/pweb/windows/flutter/generated_plugin_registrant.cc b/frontend/pweb/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..a6cdd48 --- /dev/null +++ b/frontend/pweb/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterTimezonePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/frontend/pweb/windows/flutter/generated_plugin_registrant.h b/frontend/pweb/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/frontend/pweb/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend/pweb/windows/flutter/generated_plugins.cmake b/frontend/pweb/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..42d0efb --- /dev/null +++ b/frontend/pweb/windows/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + flutter_timezone + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend/pweb/windows/runner/CMakeLists.txt b/frontend/pweb/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/frontend/pweb/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/frontend/pweb/windows/runner/Runner.rc b/frontend/pweb/windows/runner/Runner.rc new file mode 100644 index 0000000..7548fd9 --- /dev/null +++ b/frontend/pweb/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "web" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "web" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "web.exe" "\0" + VALUE "ProductName", "web" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/frontend/pweb/windows/runner/flutter_window.cpp b/frontend/pweb/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/frontend/pweb/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/frontend/pweb/windows/runner/flutter_window.h b/frontend/pweb/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/frontend/pweb/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/frontend/pweb/windows/runner/main.cpp b/frontend/pweb/windows/runner/main.cpp new file mode 100644 index 0000000..0988e65 --- /dev/null +++ b/frontend/pweb/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"web", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/frontend/pweb/windows/runner/resource.h b/frontend/pweb/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/frontend/pweb/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/frontend/pweb/windows/runner/resources/app_icon.ico b/frontend/pweb/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/frontend/pweb/windows/runner/runner.exe.manifest b/frontend/pweb/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/frontend/pweb/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/frontend/pweb/windows/runner/utils.cpp b/frontend/pweb/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/frontend/pweb/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/frontend/pweb/windows/runner/utils.h b/frontend/pweb/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/frontend/pweb/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/frontend/pweb/windows/runner/win32_window.cpp b/frontend/pweb/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/frontend/pweb/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/frontend/pweb/windows/runner/win32_window.h b/frontend/pweb/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/frontend/pweb/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ -- 2.49.1 From aac6cd2b9b14d023c597165123a3ef468c258756 Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 13 Nov 2025 15:07:43 +0300 Subject: [PATCH 3/3] deletes ds_store files --- .DS_Store | Bin 10244 -> 10244 bytes infra/.DS_Store | Bin 8196 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 infra/.DS_Store diff --git a/.DS_Store b/.DS_Store index 9a280bb577904be03cdc7b9982d25a54499f2a1a..cd92a4c5339fa0b7d0289b3db35fd4f1cabb7201 100644 GIT binary patch delta 185 zcmZn(XbG6$&&abeU^hP_&t@Khm8^ms3~3BS4EYRs3?&SyKr&_W0ntWbb3+{kBNMY) q9ffL3BLf`;6El;^b;8D&syA;Fe$9wQc$sJ}`(}29b=0sRjS&Fo!Y$bV delta 38 scmZn(XbG6$&&atkU^hP_=Vl&(m8_drO2n~mZ1}>onO$KWi097)00No~HUIzs diff --git a/infra/.DS_Store b/infra/.DS_Store deleted file mode 100644 index e8acf6f25c351f86325870c33c03095bdc7decfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMPfrs;6o13tEr=|oAWAfw)Of;xKnTQxYrz=QSYi>2fOXprb;EY2+1*kQNqW|! zA3*PZ0WT&V{Q`P2@$ShF;K4V4Dp26y#VE{dW`1wqyf@SL+u1*F3IHTkEnWi{0ssRm zN3$E7BMR^5^-yV1nhr#Q{s2zC7FO~e%OA4U3eA9KKr^5j&{tX83oz07J%zIz< z>Q*zL890>;@aKbtm7}e&E>S!>un{Q$VjHSOK^gM^;bSSZ71kvRR~%Dj4@9{Vm0}R- zj{O!lN3<2zB}#W7(jADJnWzkfh}qH466Qc`iMrJcXa){5z;pL@D1i$R&;$AV`+UW7 z!)R>m3u$R>YwzeZI*o2)E%S`kGLaKi!;}-O@Uun6ynI7;@=GqQ<;=t^^CQRgX&@Zj zB1MzcN3LIFwG^xR#ZdTWWQP$mVmWhYXJ>q7YQ&nDp4=U=c4o%MMy$z+somX}F_0X+ z{vfyEm3{V720_$=0D78&dUY@&x3H;pB*@oBMBmK5Moh7;?lV1Sd(FOheM^ zoEtiSVR-mr&g{=Sg^g0cf9)*uSdj9nbfpxy#Z1L1xPB=c(XA-sKBqY|-emfK7N6{? z7Pl*Clh{*;;cf?%`eMBF`g%yC>^^xpXZD7LfO*~m3*E?N{_kKE~M9R|@ZWS)kAVfUmo?Ui=!0}5owY}y!dFrM86pJEO=9YZ40`nIF%16q&DD$wA zH-1QNllI-ZkdpRgsWBlb1^j$WG$e&2`LX;~$cw&Hrmlhou0Esd*wNsXw0)KS>CjyE%b z?=ioGTJF&WHDGuYp@OTt02|l_`}V5-=e2%hYN6{)~ZNHFv?0 -- 2.49.1