From 3488c3bc3d34b453b9119ee1e87f8c678ffc53a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Koby=C5=82ko?= Date: Tue, 24 Jan 2023 17:45:28 +0100 Subject: [PATCH] Migrate to Compose Material3 wip --- about/build.gradle.kts | 26 -- about/src/main/AndroidManifest.xml | 9 - .../com/twofasapp/about/ui/AboutActivity.kt | 204 ---------- .../com/twofasapp/about/ui/AboutUiState.kt | 5 - .../com/twofasapp/about/ui/AboutViewModel.kt | 41 --- app/build.gradle.kts | 20 +- app/src/main/AndroidManifest.xml | 20 +- .../main/java/com/twofasapp/ActivityModule.kt | 14 +- app/src/main/java/com/twofasapp/App.kt | 10 - .../main/java/com/twofasapp/AppConfigImpl.kt | 24 -- app/src/main/java/com/twofasapp/AppModule.kt | 4 - .../java/com/twofasapp/NavigationModule.kt | 9 +- .../main/java/com/twofasapp/UseCaseModule.kt | 2 - .../main/java/com/twofasapp/di/AppModule.kt | 15 + app/src/main/java/com/twofasapp/di/Modules.kt | 20 +- .../com/twofasapp/environment/AppBuildImpl.kt | 27 ++ .../addserviceqr/AddServiceQrPresenter.kt | 13 +- .../twofasapp/features/main/MainPresenter.kt | 10 +- .../features/main/MainServicesActivity.kt | 220 +++++------ .../navigator/ActivityScopedNavigator.kt | 21 +- .../features/services/NoServicesItem.kt | 8 +- .../features/trash/EmptyTrashItem.kt | 17 - .../twofasapp/features/trash/TrashActivity.kt | 31 -- .../twofasapp/features/trash/TrashContract.kt | 17 - .../features/trash/TrashPresenter.kt | 54 --- .../features/trash/TrashedService.kt | 7 - .../features/trash/TrashedServiceItem.kt | 45 --- .../trash/delete/DisposeServiceActivity.kt | 58 --- .../trash/delete/DisposeServiceContract.kt | 21 -- .../trash/delete/DisposeServicePresenter.kt | 29 -- .../navigation/ExternalImportRouterImpl.kt | 85 ----- .../navigation/SettingsRouterImpl.kt | 76 ---- .../twofasapp/navigation/StartRouterImpl.kt | 2 - .../backup/usecases/UpdateRemoteBackup.kt | 8 +- .../com/twofasapp/time/TimeProviderImpl.kt | 21 ++ .../java/com/twofasapp/ui/main/MainNavHost.kt | 81 +++- .../java/com/twofasapp/ui/main/MainScreen.kt | 4 +- .../com/twofasapp/ui/main/MainViewModel.kt | 7 + .../usecases/backup/SyncBackupServices.kt | 7 +- .../usecases/services/GetTrashedServices.kt | 17 - app/src/main/res/layout/activity_trash.xml | 29 -- app/src/main/res/layout/item_no_services.xml | 2 +- .../main/res/menu/menu_trashed_service.xml | 9 - backup/build.gradle.kts | 5 +- backup/domain/build.gradle.kts | 2 +- .../backup/domain/ExportBackupSuspended.kt | 9 +- .../backup/domain/ExportBackupToDisk.kt | 9 +- .../backup/ui/export/ExportBackupActivity.kt | 4 +- base/build.gradle.kts | 2 +- browserextension/build.gradle.kts | 11 +- browserextension/domain/build.gradle.kts | 17 - .../domain/src/main/AndroidManifest.xml | 4 - .../domain/FetchTokenRequestsCase.kt | 7 - .../domain/ObserveMobileDeviceCase.kt | 8 - .../domain/ObservePairedBrowsersCase.kt | 8 - .../domain/model/PairedBrowser.kt | 16 - .../BrowserExtensionModule.kt | 37 +- .../data/BrowserExtensionLocalData.kt | 13 - .../data/BrowserExtensionLocalDataImpl.kt | 72 ---- .../data/BrowserExtensionRemoteData.kt | 56 --- .../data/BrowserExtensionRemoteDataImpl.kt | 106 ------ .../domain/ApproveLoginRequestCase.kt | 6 +- .../domain/DeletePairedBrowserCase.kt | 6 +- .../domain/DenyLoginRequestCase.kt | 6 +- .../domain/FetchPairedBrowsersCase.kt | 6 +- .../domain/FetchTokenRequestsCase.kt | 15 + .../domain/FetchTokenRequestsCaseImpl.kt | 15 - .../domain/ObserveMobileDeviceCase.kt | 14 + .../domain/ObserveMobileDeviceCaseImpl.kt | 14 - .../domain/ObservePairedBrowsersCase.kt | 14 + .../domain/ObservePairedBrowsersCaseImpl.kt | 14 - .../domain/PairBrowserCase.kt | 6 +- .../domain/RegisterMobileDeviceCase.kt | 14 +- .../domain/UpdateMobileDeviceCase.kt | 6 +- .../BrowserExtensionRepositoryImpl.kt | 84 ----- .../ui/browser/BrowserDetailsScreen.kt | 39 +- .../ui/browser/BrowserDetailsScreenFactory.kt | 12 - .../ui/browser/BrowserDetailsUiState.kt | 2 +- .../ui/browser/BrowserDetailsViewModel.kt | 14 +- .../ui/main/BrowserExtensionScreen.kt | 196 ++++------ .../ui/main/BrowserExtensionScreenFactory.kt | 11 - .../ui/main/BrowserExtensionUiState.kt | 6 +- .../ui/main/BrowserExtensionViewModel.kt | 21 +- .../pairing/progress/PairingProgressScreen.kt | 72 ++-- .../progress/PairingProgressScreenFactory.kt | 11 - .../progress/PairingProgressUiState.kt | 2 +- .../progress/PairingProgressViewModel.kt | 4 +- .../ui/pairing/scan/PairingScanScreen.kt | 31 +- .../pairing/scan/PairingScanScreenFactory.kt | 11 - .../ui/pairing/scan/PairingScanUiState.kt | 2 +- .../ui/pairing/scan/PairingScanViewModel.kt | 12 +- build.gradle.kts | 4 +- buildlogic/build.gradle.kts | 4 +- .../buildlogic/extension/KotlinAndroid.kt | 2 + core/common/build.gradle.kts | 2 +- .../twofasapp/common/environment/AppBuild.kt | 8 +- .../common/environment/BuildVariant.kt | 7 + .../java/com/twofasapp/common/ktx/Base64.kt | 79 ++++ .../com/twofasapp/common/ktx/CoroutinesKtx.kt | 22 ++ .../com/twofasapp/common/ktx/NavigationKtx.kt | 9 + .../com/twofasapp/common/ktx/StringKtx.kt | 5 + .../twofasapp/common/navigation/NavNode.kt | 12 +- .../com/twofasapp/common/time/TimeProvider.kt | 9 + core/designsystem/build.gradle.kts | 1 + .../com/twofasapp/designsystem/AppTheme.kt | 28 +- .../com/twofasapp/designsystem/TwIcons.kt | 37 ++ .../com/twofasapp/designsystem/TwTheme.kt | 31 ++ .../com/twofasapp/designsystem/TwsColors.kt | 20 - .../twofasapp/designsystem/TwsColorsDark.kt | 16 - .../twofasapp/designsystem/TwsColorsLight.kt | 16 - .../com/twofasapp/designsystem/TwsTheme.kt | 21 -- .../com/twofasapp/designsystem/TwsTypo.kt | 20 - .../twofasapp/designsystem/common/Button.kt | 126 +++++++ .../designsystem/common/DropdownMenu.kt | 72 ++++ .../designsystem/common/HeaderItem.kt | 32 ++ .../com/twofasapp/designsystem/common/Lazy.kt | 27 ++ .../twofasapp/designsystem/common/Modal.kt | 65 ++++ .../designsystem/common/ModalBottomSheet.kt | 39 ++ .../{composable => common}/NavigationBar.kt | 35 +- .../twofasapp/designsystem/common/Progress.kt | 18 + .../designsystem/common/SimpleItem.kt | 105 ++++++ .../designsystem/common/TopAppBar.kt | 196 ++++++++++ .../designsystem/composable/Button.kt | 58 --- .../designsystem/internal/ThemeColors.kt | 39 ++ .../designsystem/internal/ThemeColorsDark.kt | 18 + .../designsystem/internal/ThemeColorsLight.kt | 17 + .../{TwsDimen.kt => internal/ThemeDimens.kt} | 7 +- .../{TwsShape.kt => internal/ThemeShapes.kt} | 6 +- .../designsystem/internal/ThemeTypo.kt | 83 +++++ .../twofasapp/designsystem/ktx/ContextKtx.kt | 36 ++ .../twofasapp/designsystem/ktx/ImageKtx.kt | 27 ++ .../twofasapp/designsystem/lazy/ListItem.kt | 35 ++ .../designsystem/reorderable/DetectReorder.kt | 60 +++ .../reorderable/DragCancelledAnimation.kt | 57 +++ .../designsystem/reorderable/DragGesture.kt | 165 +++++++++ .../designsystem/reorderable/DraggedItem.kt | 41 +++ .../designsystem/reorderable/ItemPosition.kt | 3 + .../designsystem/reorderable/Move.kt | 34 ++ .../designsystem/reorderable/Reorderable.kt | 83 +++++ .../reorderable/ReorderableItem.kt | 91 +++++ .../reorderable/ReorderableLazyGridState.kt | 101 +++++ .../reorderable/ReorderableLazyListState.kt | 160 ++++++++ .../reorderable/ReorderableState.kt | 348 ++++++++++++++++++ .../designsystem/screen/CommonContent.kt | 120 ++++++ .../twofasapp/designsystem/service/Service.kt | 82 +++++ .../designsystem/service/ServiceImageType.kt | 3 + .../designsystem/service/ServiceModal.kt | 107 ++++++ .../designsystem/service/ServiceNoCode.kt | 77 ++++ .../designsystem/service/ServiceNormal.kt | 104 ++++++ .../designsystem/service/ServiceState.kt | 18 + .../designsystem/service/ServiceStyle.kt | 7 + .../service/component/ServiceBadge.kt | 20 + .../service/component/ServiceData.kt | 105 ++++++ .../service/component/ServiceImage.kt | 72 ++++ .../service/component/ServiceTimer.kt | 29 ++ .../settings/SettingsDescription.kt | 34 ++ .../designsystem/settings/SettingsDivider.kt | 22 ++ .../designsystem/settings/SettingsHeader.kt | 33 ++ .../designsystem/settings/SettingsLink.kt | 69 ++++ .../src/main/res/drawable/ic_add.xml | 5 + .../src/main/res/drawable/ic_cloud_upload.xml | 5 + .../src/main/res/drawable/ic_copy.xml | 5 + .../src/main/res/drawable/ic_delete.xml | 5 + .../src/main/res/drawable/ic_drag_handle.xml | 5 + .../src/main/res/drawable/ic_edit.xml | 5 + .../src/main/res/drawable/ic_extension.xml | 5 + .../main/res/drawable/ic_external_link.xml | 5 + .../src/main/res/drawable/ic_eye.xml | 5 + .../src/main/res/drawable/ic_favorite.xml | 5 + .../src/main/res/drawable/ic_file_upload.xml | 5 + .../src/main/res/drawable/ic_home.xml | 5 + .../src/main/res/drawable/ic_info.xml | 5 + .../src/main/res/drawable/ic_licenses.xml | 5 + .../src/main/res/drawable/ic_lock.xml | 5 + .../src/main/res/drawable/ic_lock_open.xml | 5 + .../src/main/res/drawable/ic_more.xml | 5 + .../src/main/res/drawable/ic_notification.xml | 5 + .../src/main/res/drawable/ic_placeholder.xml | 10 + .../src/main/res/drawable/ic_qr.xml | 15 + .../src/main/res/drawable/ic_security.xml | 5 + .../src/main/res/drawable/ic_settings.xml | 5 + .../src/main/res/drawable/ic_share.xml | 5 + .../src/main/res/drawable/ic_support.xml | 5 + .../src/main/res/drawable/ic_terms.xml | 5 + .../src/main/res/drawable/ic_write.xml | 5 + .../src/main/res/drawable/logo_2fas.xml | 36 ++ .../src/main/res/drawable/logo_aegis.webp | Bin 0 -> 8180 bytes .../drawable/logo_google_authenticator.xml | 34 ++ .../src/main/res/drawable/logo_raivo.webp | Bin 0 -> 18048 bytes {about => core/di}/.gitignore | 0 {di => core/di}/build.gradle.kts | 0 {di => core/di}/src/main/AndroidManifest.xml | 0 .../main/java/com/twofasapp/di/KoinModule.kt | 0 .../com/twofasapp/di/extensions/ScopedOf.kt | 0 .../main/java/com/twofasapp/locale/Links.kt | 4 + .../main/java/com/twofasapp/locale/Strings.kt | 68 ++++ .../java/com/twofasapp/locale/TwLocale.kt | 57 +++ .../java/com/twofasapp/locale/TwsLocale.kt | 12 - .../src/main/res/values/strings-duration.xml | 31 ++ .../domain => core/network}/.gitignore | 0 {network => core/network}/build.gradle.kts | 11 +- .../network}/src/main/AndroidManifest.xml | 3 +- .../com/twofasapp/network/di/NetworkModule.kt | 55 +++ {di => core/otp}/.gitignore | 0 core/otp/build.gradle.kts | 13 + .../otp}/src/main/AndroidManifest.xml | 3 +- .../com/twofasapp/otp/OtpAuthenticator.kt | 117 ++++++ .../main/java/com/twofasapp/otp/OtpData.kt | 11 + .../java/com/twofasapp/otp/OtpException.kt | 3 + .../com/twofasapp/core/encoding/Base64.kt | 4 +- core/storage/build.gradle.kts | 2 +- .../java/com/twofasapp/storage/Preferences.kt | 4 + ...{PreferencesModule.kt => StorageModule.kt} | 2 +- .../storage/internal/PreferencesDelegate.kt | 31 ++ {environment => data/browserext}/.gitignore | 0 data/browserext/build.gradle.kts | 21 ++ .../browserext}/src/main/AndroidManifest.xml | 3 +- .../data/browserext/BrowserExtRepository.kt | 11 +- .../browserext/BrowserExtRepositoryImpl.kt | 100 +++++ .../browserext/di/DataBrowserExtModule.kt | 18 + .../data/browserext/domain}/MobileDevice.kt | 2 +- .../data/browserext/domain/PairedBrowser.kt | 10 + .../data/browserext/domain}/TokenRequest.kt | 2 +- .../browserext/local/BrowserExtLocalSource.kt | 78 ++++ .../browserext/local}/PairedBrowserDao.kt | 4 +- .../local}/model/MobileDeviceEntity.kt | 4 +- .../local}/model/PairedBrowserEntity.kt | 2 +- .../browserext/mapper/MobileDeviceMapper.kt | 20 + .../browserext/mapper/PairBrowserMapper.kt | 24 ++ .../remote/BrowserExtRemoteSource.kt | 100 +++++ .../BrowserAlreadyPairedException.kt | 2 +- .../remote/model}/ApproveLoginRequestBody.kt | 4 +- .../browserext/remote/model/BrowserJson.kt | 4 +- .../remote/model}/DenyLoginRequestBody.kt | 4 +- .../remote/model}/PairBrowserBody.kt | 4 +- .../remote/model/PairBrowserJson.kt | 4 +- .../remote/model/RegisterDeviceBody.kt | 4 +- .../remote/model/RegisterDeviceJson.kt | 4 +- .../remote/model/TokenRequestJson.kt | 4 +- .../notifications}/.gitignore | 0 data/notifications/build.gradle.kts | 20 + .../src/main/AndroidManifest.xml | 4 + .../notifications/NotificationsRepository.kt | 11 + .../NotificationsRepositoryImpl.kt | 54 +++ .../di/DataNotificationsModule.kt | 18 + .../notifications/domain}/Notification.kt | 4 +- .../notifications/local/NotificationsDao.kt | 6 +- .../local/NotificationsLocalSource.kt | 36 ++ .../local}/model/NotificationEntity.kt | 2 +- .../mappper/NotificationsMapper.kt | 17 +- .../remote/NotificationsRemoteSource.kt | 21 ++ .../remote/model/NotificationJson.kt | 4 +- {network => data/services}/.gitignore | 0 data/services/build.gradle.kts | 23 ++ data/services/src/main/AndroidManifest.xml | 4 + .../data/services/GroupsRepository.kt | 8 + .../data/services/GroupsRepositoryImpl.kt | 11 + .../data/services/ServicesRepository.kt | 16 + .../data/services/ServicesRepositoryImpl.kt | 79 ++++ .../data/services/di/DataServicesModule.kt | 22 ++ .../twofasapp/data/services/domain/Group.kt | 9 + .../twofasapp/data/services/domain/Service.kt | 95 +++++ .../data/services/local/GroupsLocalSource.kt | 11 + .../data/services/local/ServiceDao.kt | 64 ++++ .../services/local/ServicesLocalSource.kt | 193 ++++++++++ .../services/local}/model/ServiceEntity.kt | 2 +- .../data/services/mapper/ServiceMapper.kt | 64 ++++ .../data/services/otp/ServiceCodeGenerator.kt | 59 +++ data/session/build.gradle.kts | 2 +- .../data/session/SessionRepository.kt | 1 + .../data/session/SessionRepositoryImpl.kt | 12 +- .../data/session/SettingsRepository.kt | 9 + .../data/session/SettingsRepositoryImpl.kt | 18 + .../data/session/di/DataSessionModule.kt | 9 +- .../data/session/domain/AppSettings.kt | 5 + .../data/session/local/SessionLocalSource.kt | 18 +- .../session/local/SessionLocalSourceImpl.kt | 18 - .../data/session/local/SettingsLocalSource.kt | 31 ++ design/build.gradle.kts | 4 +- .../twofasapp/design/compose/SimpleEntry.kt | 21 +- .../twofasapp/design/compose/SwitchEntry.kt | 13 +- .../com/twofasapp/design/compose/Toolbar.kt | 18 +- .../design/compose/dialogs/InputDialog.kt | 1 - .../design/compose/dialogs/ListDialog.kt | 4 +- .../java/com/twofasapp/design/theme/Color.kt | 14 +- developer/build.gradle.kts | 4 +- environment/build.gradle.kts | 11 - .../com/twofasapp/environment/BuildVariant.kt | 5 - externalimport/src/main/AndroidManifest.xml | 12 - .../externalimport/ExternalImportModule.kt | 37 -- .../ui/ExternalImportActivity.kt | 24 -- .../externalimport/ui/aegis/AegisScreen.kt | 40 -- .../ui/aegis/AegisScreenFactory.kt | 12 - .../GoogleAuthenticatorScreenFactory.kt | 11 - .../ui/main/ExternalImportScreen.kt | 69 ---- .../ui/main/ExternalImportScreenFactory.kt | 11 - .../externalimport/ui/raivo/RaivoScreen.kt | 38 -- .../ui/raivo/RaivoScreenFactory.kt | 13 - .../ui/result/ImportResultScreenFactory.kt | 11 - .../ui/scan/ImportScanScreenFactory.kt | 11 - {notifications => feature/about}/.gitignore | 0 feature/about/build.gradle.kts | 23 ++ feature/about/src/main/AndroidManifest.xml | 4 + .../src/main/assets/open_source_licenses.html | 0 .../feature/about/di}/AboutModule.kt | 5 +- .../about/navigation/AboutNavigation.kt | 40 ++ .../feature/about/ui/about/AboutScreen.kt | 112 ++++++ .../feature/about/ui/about/AboutViewModel.kt | 34 ++ .../about/ui/licenses/LicensesScreen.kt | 46 +++ {settings => feature/appsettings}/.gitignore | 0 feature/appsettings/build.gradle.kts | 21 ++ .../appsettings/src/main/AndroidManifest.xml | 4 + .../appsettings/di/AppSettingsModule.kt | 12 + .../navigation/AppSettingsNavigation.kt | 18 + .../appsettings/ui/AppSettingsScreen.kt | 63 ++++ .../appsettings/ui/AppSettingsUiState.kt | 4 + .../appsettings/ui/AppSettingsViewModel.kt | 55 +++ feature/browserext/.gitignore | 1 + feature/browserext/build.gradle.kts | 23 ++ .../browserext/src/main/AndroidManifest.xml | 4 + .../notification/BrowserExtNavigation.kt | 88 +++++ feature/externalimport/.gitignore | 1 + .../externalimport}/build.gradle.kts | 13 +- .../src/main/AndroidManifest.xml | 2 + .../externalimport/di/ExternalImportModule.kt | 23 ++ .../externalimport/domain/AegisImporter.kt | 4 +- .../externalimport/domain/ExternalImport.kt | 2 +- .../externalimport/domain/ExternalImporter.kt | 2 +- .../domain/GoogleAuthenticatorImporter.kt | 2 +- .../externalimport/domain/RaivoImporter.kt | 4 +- .../navigation/ExternalImportNavigation.kt | 122 ++++++ .../externalimport/ui/aegis/AegisScreen.kt | 30 ++ .../ui/common/ImportComponents.kt | 43 +-- .../GoogleAuthenticatorScreen.kt | 59 +-- .../externalimport/ui/raivo/RaivoScreen.kt | 28 ++ .../ui/result/ImportResultScreen.kt | 63 ++-- .../ui/result/ImportResultUiState.kt | 4 +- .../ui/result/ImportResultViewModel.kt | 45 ++- .../ui/scan/ImportScanScreen.kt | 33 +- .../ui/scan/ImportScanUiState.kt | 2 +- .../ui/scan/ImportScanViewModel.kt | 4 +- .../ui/selector/SelectorScreen.kt | 74 ++++ .../src/main/proto/google_authenticator.proto | 0 feature/home/build.gradle.kts | 5 +- .../twofasapp/feature/home/di/HomeModule.kt | 9 + .../feature/home/navigation/HomeNavigation.kt | 84 ++++- .../navigation/NotificationsNavigation.kt | 23 -- .../home/navigation/ServicesNavigation.kt | 23 -- .../home/navigation/SettingsNavigation.kt | 23 -- .../twofasapp/feature/home/ui/HomeScreen.kt | 85 ----- .../feature/home/ui/bottombar/BottomBar.kt | 76 ++++ .../bottombar}/BottomNavItem.kt | 2 +- .../ui/notifications/NotificationsScreen.kt | 149 ++++++++ .../notifications/NotificationsViewModel.kt | 43 +++ .../feature/home/ui/services/AppBar.kt | 142 +++++++ .../feature/home/ui/services/Empty.kt | 34 ++ .../twofasapp/feature/home/ui/services/Fab.kt | 51 +++ .../feature/home/ui/services/ListItem.kt | 13 + .../feature/home/ui/services/ModalType.kt | 6 + .../feature/home/ui/services/Progress.kt | 16 + .../home/ui/services/ServicesScreen.kt | 245 ++++++++++++ .../home/ui/services/ServicesUiState.kt | 19 + .../home/ui/services/ServicesViewModel.kt | 57 +++ .../feature/home/ui/services/StateMapper.kt | 45 +++ .../home/ui/services/modal/AddServiceModal.kt | 17 + .../ui/services/modal/FocusServiceModal.kt | 34 ++ .../home/ui/settings/SettingsScreen.kt | 113 ++++++ .../home/ui/settings/SettingsViewModel.kt | 9 + .../res/drawable-night/img_services_empty.xml | 260 +++++++++++++ .../home/src/main/res/drawable/ic_android.xml | 5 - .../main/res/drawable/img_services_empty.xml | 260 +++++++++++++ .../res/drawable/notif_category_feature.xml | 0 .../main/res/drawable/notif_category_news.xml | 0 .../res/drawable/notif_category_update.xml | 0 .../res/drawable/notif_category_video.xml | 0 feature/startup/build.gradle.kts | 2 +- .../startup/navigation/StartupNavigation.kt | 4 +- .../feature/startup/ui/StartupScreen.kt | 83 ++--- feature/trash/.gitignore | 1 + feature/trash/build.gradle.kts | 21 ++ feature/trash/src/main/AndroidManifest.xml | 4 + .../twofasapp/feature/trash/di/TrashModule.kt | 14 + .../trash/navigation/TrashNavigation.kt | 50 +++ .../feature/trash/ui/dispose/DisposeScreen.kt | 40 ++ .../trash/ui/dispose/DisposeViewModel.kt | 21 ++ .../feature/trash/ui/trash/TrashScreen.kt | 136 +++++++ .../feature/trash/ui/trash/TrashViewModel.kt | 33 ++ featuretoggle/build.gradle.kts | 5 +- .../domain/IsFeatureEnabledCaseImpl.kt | 8 +- .../repository/RemoteConfigRepositoryImpl.kt | 6 +- gradle/libs.versions.toml | 11 +- .../navigation/ExternalImportDirections.kt | 16 - .../navigation/ExternalImportRouter.kt | 5 - .../navigation/SettingsDirections.kt | 13 - .../twofasapp/navigation/SettingsRouter.kt | 5 - .../twofasapp/navigation/StartDirections.kt | 1 - .../com/twofasapp/network/NetworkModule.kt | 63 ---- .../network/api/BrowserExtensionApi.kt | 78 ---- .../twofasapp/network/api/NotificationsApi.kt | 24 -- .../twofasapp/network/config/HostVerifier.kt | 14 - .../network/config/SslSocketFactory.kt | 16 - .../network/config/TrustManagerSelfSigned.kt | 10 - notifications/build.gradle.kts | 29 -- .../notifications/NotificationsModule.kt | 32 -- .../data/NotificationsLocalData.kt | 12 - .../data/NotificationsLocalDataImpl.kt | 37 -- .../data/NotificationsRemoteData.kt | 9 - .../data/NotificationsRemoteDataImpl.kt | 17 - .../domain/FetchNotificationsCase.kt | 5 - .../domain/FetchNotificationsCaseImpl.kt | 17 - .../domain/GetNotificationsCase.kt | 19 - .../domain/HasUnreadNotificationsCase.kt | 7 - .../domain/HasUnreadNotificationsCaseImpl.kt | 15 - .../domain/ObserveNotificationsCase.kt | 24 -- .../domain/ReadAllNotificationsCase.kt | 12 - .../repository/NotificationsRepository.kt | 16 - .../repository/NotificationsRepositoryImpl.kt | 38 -- .../notifications/ui/NotificationsActivity.kt | 153 -------- .../notifications/ui/NotificationsUiState.kt | 19 - .../ui/NotificationsViewModel.kt | 59 --- parsers/build.gradle.kts | 2 +- permissions/build.gradle.kts | 2 +- persistence/build.gradle.kts | 8 +- .../com/twofasapp/persistence/AppDatabase.kt | 14 +- .../persistence/PersistenceModule.kt | 4 +- .../twofasapp/persistence/dao/ServiceDao.kt | 45 --- prefs/build.gradle.kts | 4 +- .../twofasapp/prefs/PreferencesPlainModule.kt | 2 - .../prefs/usecase/MobileDevicePreference.kt | 20 - push/build.gradle.kts | 5 +- .../push/domain/repository/PushLogger.kt | 8 +- qrscanner/build.gradle.kts | 2 +- .../twofasapp/qrscanner/ui/QrScannerScreen.kt | 14 +- resources/src/main/res/values/colors.xml | 2 +- security/build.gradle.kts | 4 +- security/domain/build.gradle.kts | 5 +- .../security/ui/changepin/ChangePinScreen.kt | 6 +- .../ui/disablepin/DisablePinScreen.kt | 6 +- .../com/twofasapp/security/ui/pin/PinInput.kt | 23 +- .../twofasapp/security/ui/pin/PinKeyboard.kt | 11 +- .../twofasapp/security/ui/pin/PinScreen.kt | 26 +- .../security/ui/security/SecurityActivity.kt | 4 +- .../security/ui/security/SecurityScreen.kt | 43 ++- .../security/ui/setuppin/SetupPinScreen.kt | 17 +- serialization/build.gradle.kts | 2 +- services/build.gradle.kts | 6 +- services/domain/build.gradle.kts | 6 +- .../domain/otp/InvalidSecretFormat.kt | 3 + .../services/data/ServicesLocalDataImpl.kt | 31 +- .../data/converter/ServiceConverter.kt | 6 +- settings.gradle.kts | 18 +- settings/build.gradle.kts | 23 -- settings/src/main/AndroidManifest.xml | 12 - .../com/twofasapp/settings/SettingsModule.kt | 21 -- .../twofasapp/settings/ui/SettingsActivity.kt | 24 -- .../settings/ui/main/SettingsMainScreen.kt | 66 ---- .../ui/main/SettingsMainScreenFactory.kt | 11 - .../settings/ui/main/SettingsMainUiState.kt | 8 - .../settings/ui/main/SettingsMainViewModel.kt | 44 --- .../settings/ui/theme/ThemeScreen.kt | 72 ---- .../settings/ui/theme/ThemeScreenFactory.kt | 11 - .../settings/ui/theme/ThemeUiState.kt | 8 - .../settings/ui/theme/ThemeViewModel.kt | 41 --- start/build.gradle.kts | 5 +- .../java/com/twofasapp/start/StartModule.kt | 4 - .../start/ui/onboarding/OnboardingActivity.kt | 114 ------ .../ui/onboarding/OnboardingViewModel.kt | 37 -- .../onboarding/step/OnboardingStepFragment.kt | 47 --- .../twofasapp/start/ui/start/StartActivity.kt | 2 +- .../twofasapp/start/work/OnAppUpdatedWork.kt | 10 +- .../main/res/layout/activity_onboarding.xml | 87 ----- .../res/layout/fragment_onboarding_step.xml | 61 --- time/build.gradle.kts | 3 +- time/domain/build.gradle.kts | 3 - widgets/build.gradle.kts | 4 +- widgets/domain/build.gradle.kts | 5 +- 476 files changed, 9086 insertions(+), 4438 deletions(-) delete mode 100644 about/build.gradle.kts delete mode 100644 about/src/main/AndroidManifest.xml delete mode 100644 about/src/main/java/com/twofasapp/about/ui/AboutActivity.kt delete mode 100644 about/src/main/java/com/twofasapp/about/ui/AboutUiState.kt delete mode 100644 about/src/main/java/com/twofasapp/about/ui/AboutViewModel.kt delete mode 100644 app/src/main/java/com/twofasapp/AppConfigImpl.kt create mode 100644 app/src/main/java/com/twofasapp/environment/AppBuildImpl.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/EmptyTrashItem.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/TrashActivity.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/TrashContract.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/TrashPresenter.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/TrashedService.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/TrashedServiceItem.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceActivity.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceContract.kt delete mode 100644 app/src/main/java/com/twofasapp/features/trash/delete/DisposeServicePresenter.kt delete mode 100644 app/src/main/java/com/twofasapp/navigation/ExternalImportRouterImpl.kt delete mode 100644 app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt create mode 100644 app/src/main/java/com/twofasapp/time/TimeProviderImpl.kt delete mode 100644 app/src/main/java/com/twofasapp/usecases/services/GetTrashedServices.kt delete mode 100644 app/src/main/res/layout/activity_trash.xml delete mode 100644 app/src/main/res/menu/menu_trashed_service.xml delete mode 100644 browserextension/domain/build.gradle.kts delete mode 100644 browserextension/domain/src/main/AndroidManifest.xml delete mode 100644 browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt delete mode 100644 browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt delete mode 100644 browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt delete mode 100644 browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/PairedBrowser.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalData.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalDataImpl.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteData.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteDataImpl.kt create mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCaseImpl.kt create mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCaseImpl.kt create mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCaseImpl.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepositoryImpl.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreenFactory.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreenFactory.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreenFactory.kt delete mode 100644 browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreenFactory.kt rename environment/src/main/java/com/twofasapp/environment/AppConfig.kt => core/common/src/main/java/com/twofasapp/common/environment/AppBuild.kt (59%) create mode 100644 core/common/src/main/java/com/twofasapp/common/environment/BuildVariant.kt create mode 100644 core/common/src/main/java/com/twofasapp/common/ktx/Base64.kt create mode 100644 core/common/src/main/java/com/twofasapp/common/ktx/CoroutinesKtx.kt create mode 100644 core/common/src/main/java/com/twofasapp/common/ktx/NavigationKtx.kt create mode 100644 core/common/src/main/java/com/twofasapp/common/ktx/StringKtx.kt create mode 100644 core/common/src/main/java/com/twofasapp/common/time/TimeProvider.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/TwIcons.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/TwTheme.kt delete mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColors.kt delete mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsDark.kt delete mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsLight.kt delete mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTheme.kt delete mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTypo.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/Button.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/DropdownMenu.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/HeaderItem.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/Lazy.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/Modal.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/ModalBottomSheet.kt rename core/designsystem/src/main/java/com/twofasapp/designsystem/{composable => common}/NavigationBar.kt (53%) create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/Progress.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/SimpleItem.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/common/TopAppBar.kt delete mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/composable/Button.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColors.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsDark.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsLight.kt rename core/designsystem/src/main/java/com/twofasapp/designsystem/{TwsDimen.kt => internal/ThemeDimens.kt} (60%) rename core/designsystem/src/main/java/com/twofasapp/designsystem/{TwsShape.kt => internal/ThemeShapes.kt} (72%) create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeTypo.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ContextKtx.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ImageKtx.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/lazy/ListItem.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DetectReorder.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragCancelledAnimation.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragGesture.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DraggedItem.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ItemPosition.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Move.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Reorderable.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableItem.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyGridState.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyListState.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableState.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/screen/CommonContent.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/Service.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceImageType.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceModal.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNoCode.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNormal.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceState.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceStyle.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceBadge.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceData.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceImage.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceTimer.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDescription.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDivider.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsHeader.kt create mode 100644 core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsLink.kt create mode 100644 core/designsystem/src/main/res/drawable/ic_add.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_cloud_upload.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_copy.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_delete.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_drag_handle.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_edit.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_extension.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_external_link.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_eye.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_favorite.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_file_upload.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_home.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_info.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_licenses.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_lock.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_lock_open.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_more.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_notification.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_placeholder.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_qr.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_security.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_settings.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_share.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_support.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_terms.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_write.xml create mode 100644 core/designsystem/src/main/res/drawable/logo_2fas.xml create mode 100644 core/designsystem/src/main/res/drawable/logo_aegis.webp create mode 100644 core/designsystem/src/main/res/drawable/logo_google_authenticator.xml create mode 100644 core/designsystem/src/main/res/drawable/logo_raivo.webp rename {about => core/di}/.gitignore (100%) rename {di => core/di}/build.gradle.kts (100%) rename {di => core/di}/src/main/AndroidManifest.xml (100%) rename {di => core/di}/src/main/java/com/twofasapp/di/KoinModule.kt (100%) rename {di => core/di}/src/main/java/com/twofasapp/di/extensions/ScopedOf.kt (100%) create mode 100644 core/locale/src/main/java/com/twofasapp/locale/TwLocale.kt delete mode 100644 core/locale/src/main/java/com/twofasapp/locale/TwsLocale.kt create mode 100644 core/locale/src/main/res/values/strings-duration.xml rename {browserextension/domain => core/network}/.gitignore (100%) rename {network => core/network}/build.gradle.kts (53%) rename {network => core/network}/src/main/AndroidManifest.xml (73%) create mode 100644 core/network/src/main/java/com/twofasapp/network/di/NetworkModule.kt rename {di => core/otp}/.gitignore (100%) create mode 100644 core/otp/build.gradle.kts rename {environment => core/otp}/src/main/AndroidManifest.xml (71%) create mode 100755 core/otp/src/main/java/com/twofasapp/otp/OtpAuthenticator.kt create mode 100644 core/otp/src/main/java/com/twofasapp/otp/OtpData.kt create mode 100755 core/otp/src/main/java/com/twofasapp/otp/OtpException.kt rename core/storage/src/main/java/com/twofasapp/storage/di/{PreferencesModule.kt => StorageModule.kt} (96%) rename {environment => data/browserext}/.gitignore (100%) create mode 100644 data/browserext/build.gradle.kts rename {notifications => data/browserext}/src/main/AndroidManifest.xml (70%) rename browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepository.kt => data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepository.kt (76%) create mode 100644 data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepositoryImpl.kt create mode 100644 data/browserext/src/main/java/com/twofasapp/data/browserext/di/DataBrowserExtModule.kt rename {browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model => data/browserext/src/main/java/com/twofasapp/data/browserext/domain}/MobileDevice.kt (74%) create mode 100644 data/browserext/src/main/java/com/twofasapp/data/browserext/domain/PairedBrowser.kt rename {browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model => data/browserext/src/main/java/com/twofasapp/data/browserext/domain}/TokenRequest.kt (67%) create mode 100644 data/browserext/src/main/java/com/twofasapp/data/browserext/local/BrowserExtLocalSource.kt rename {persistence/src/main/java/com/twofasapp/persistence/dao => data/browserext/src/main/java/com/twofasapp/data/browserext/local}/PairedBrowserDao.kt (88%) rename {prefs/src/main/java/com/twofasapp/prefs => data/browserext/src/main/java/com/twofasapp/data/browserext/local}/model/MobileDeviceEntity.kt (66%) rename {persistence/src/main/java/com/twofasapp/persistence => data/browserext/src/main/java/com/twofasapp/data/browserext/local}/model/PairedBrowserEntity.kt (83%) create mode 100644 data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/MobileDeviceMapper.kt create mode 100644 data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/PairBrowserMapper.kt create mode 100644 data/browserext/src/main/java/com/twofasapp/data/browserext/remote/BrowserExtRemoteSource.kt rename {network/src/main/java/com/twofasapp/network => data/browserext/src/main/java/com/twofasapp/data/browserext/remote}/exception/BrowserAlreadyPairedException.kt (50%) rename {network/src/main/java/com/twofasapp/network/body => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model}/ApproveLoginRequestBody.kt (60%) rename network/src/main/java/com/twofasapp/network/response/BrowserResponse.kt => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/BrowserJson.kt (60%) rename {network/src/main/java/com/twofasapp/network/body => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model}/DenyLoginRequestBody.kt (50%) rename {network/src/main/java/com/twofasapp/network/body => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model}/PairBrowserBody.kt (63%) rename network/src/main/java/com/twofasapp/network/response/PairBrowserResponse.kt => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/PairBrowserJson.kt (64%) rename network/src/main/java/com/twofasapp/network/body/DeviceRegisterBody.kt => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceBody.kt (59%) rename network/src/main/java/com/twofasapp/network/response/DeviceRegisterResponse.kt => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceJson.kt (66%) rename network/src/main/java/com/twofasapp/network/response/TokenRequestResponse.kt => data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/TokenRequestJson.kt (65%) rename {externalimport => data/notifications}/.gitignore (100%) create mode 100644 data/notifications/build.gradle.kts create mode 100644 data/notifications/src/main/AndroidManifest.xml create mode 100644 data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepository.kt create mode 100644 data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepositoryImpl.kt create mode 100644 data/notifications/src/main/java/com/twofasapp/data/notifications/di/DataNotificationsModule.kt rename {notifications/src/main/java/com/twofasapp/notifications/domain/model => data/notifications/src/main/java/com/twofasapp/data/notifications/domain}/Notification.kt (81%) rename persistence/src/main/java/com/twofasapp/persistence/dao/NotificationDao.kt => data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsDao.kt (87%) create mode 100644 data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsLocalSource.kt rename {persistence/src/main/java/com/twofasapp/persistence => data/notifications/src/main/java/com/twofasapp/data/notifications/local}/model/NotificationEntity.kt (86%) rename notifications/src/main/java/com/twofasapp/notifications/domain/converter/NotificationConverter.kt => data/notifications/src/main/java/com/twofasapp/data/notifications/mappper/NotificationsMapper.kt (54%) create mode 100644 data/notifications/src/main/java/com/twofasapp/data/notifications/remote/NotificationsRemoteSource.kt rename network/src/main/java/com/twofasapp/network/response/NotificationResponse.kt => data/notifications/src/main/java/com/twofasapp/data/notifications/remote/model/NotificationJson.kt (72%) rename {network => data/services}/.gitignore (100%) create mode 100644 data/services/build.gradle.kts create mode 100644 data/services/src/main/AndroidManifest.xml create mode 100644 data/services/src/main/java/com/twofasapp/data/services/GroupsRepository.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/GroupsRepositoryImpl.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/ServicesRepository.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/ServicesRepositoryImpl.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/di/DataServicesModule.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/domain/Group.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/domain/Service.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/local/GroupsLocalSource.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/local/ServiceDao.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/local/ServicesLocalSource.kt rename {persistence/src/main/java/com/twofasapp/persistence => data/services/src/main/java/com/twofasapp/data/services/local}/model/ServiceEntity.kt (97%) create mode 100644 data/services/src/main/java/com/twofasapp/data/services/mapper/ServiceMapper.kt create mode 100644 data/services/src/main/java/com/twofasapp/data/services/otp/ServiceCodeGenerator.kt create mode 100644 data/session/src/main/java/com/twofasapp/data/session/SettingsRepository.kt create mode 100644 data/session/src/main/java/com/twofasapp/data/session/SettingsRepositoryImpl.kt create mode 100644 data/session/src/main/java/com/twofasapp/data/session/domain/AppSettings.kt delete mode 100644 data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSourceImpl.kt create mode 100644 data/session/src/main/java/com/twofasapp/data/session/local/SettingsLocalSource.kt delete mode 100644 environment/build.gradle.kts delete mode 100644 environment/src/main/java/com/twofasapp/environment/BuildVariant.kt delete mode 100644 externalimport/src/main/AndroidManifest.xml delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ExternalImportModule.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/ExternalImportActivity.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreen.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreenFactory.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreenFactory.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreen.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreenFactory.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreen.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreenFactory.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultScreenFactory.kt delete mode 100644 externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanScreenFactory.kt rename {notifications => feature/about}/.gitignore (100%) create mode 100644 feature/about/build.gradle.kts create mode 100644 feature/about/src/main/AndroidManifest.xml rename {about => feature/about}/src/main/assets/open_source_licenses.html (100%) rename {about/src/main/java/com/twofasapp/about => feature/about/src/main/java/com/twofasapp/feature/about/di}/AboutModule.kt (70%) create mode 100644 feature/about/src/main/java/com/twofasapp/feature/about/navigation/AboutNavigation.kt create mode 100644 feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutScreen.kt create mode 100644 feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutViewModel.kt create mode 100644 feature/about/src/main/java/com/twofasapp/feature/about/ui/licenses/LicensesScreen.kt rename {settings => feature/appsettings}/.gitignore (100%) create mode 100644 feature/appsettings/build.gradle.kts create mode 100644 feature/appsettings/src/main/AndroidManifest.xml create mode 100644 feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/di/AppSettingsModule.kt create mode 100644 feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/navigation/AppSettingsNavigation.kt create mode 100644 feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsScreen.kt create mode 100644 feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsUiState.kt create mode 100644 feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsViewModel.kt create mode 100644 feature/browserext/.gitignore create mode 100644 feature/browserext/build.gradle.kts create mode 100644 feature/browserext/src/main/AndroidManifest.xml create mode 100644 feature/browserext/src/main/java/com/twofasapp/feature/browserext/notification/BrowserExtNavigation.kt create mode 100644 feature/externalimport/.gitignore rename {externalimport => feature/externalimport}/build.gradle.kts (84%) create mode 100644 feature/externalimport/src/main/AndroidManifest.xml create mode 100644 feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/di/ExternalImportModule.kt rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/domain/AegisImporter.kt (98%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/domain/ExternalImport.kt (86%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/domain/ExternalImporter.kt (71%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/domain/GoogleAuthenticatorImporter.kt (98%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/domain/RaivoImporter.kt (97%) create mode 100644 feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/navigation/ExternalImportNavigation.kt create mode 100644 feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/aegis/AegisScreen.kt rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/common/ImportComponents.kt (76%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreen.kt (71%) create mode 100644 feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/raivo/RaivoScreen.kt rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/result/ImportResultScreen.kt (67%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/result/ImportResultUiState.kt (69%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/result/ImportResultViewModel.kt (66%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/scan/ImportScanScreen.kt (55%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/scan/ImportScanUiState.kt (83%) rename {externalimport/src/main/java/com/twofasapp => feature/externalimport/src/main/java/com/twofasapp/feature}/externalimport/ui/scan/ImportScanViewModel.kt (94%) create mode 100644 feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/selector/SelectorScreen.kt rename {externalimport => feature/externalimport}/src/main/proto/google_authenticator.proto (100%) delete mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/navigation/NotificationsNavigation.kt delete mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/navigation/ServicesNavigation.kt delete mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/navigation/SettingsNavigation.kt delete mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/HomeScreen.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/bottombar/BottomBar.kt rename feature/home/src/main/java/com/twofasapp/feature/home/{navigation => ui/bottombar}/BottomNavItem.kt (76%) create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsScreen.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsViewModel.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/AppBar.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Empty.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Fab.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ListItem.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ModalType.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Progress.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesScreen.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesUiState.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesViewModel.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/StateMapper.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/AddServiceModal.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/FocusServiceModal.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsScreen.kt create mode 100644 feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsViewModel.kt create mode 100644 feature/home/src/main/res/drawable-night/img_services_empty.xml delete mode 100644 feature/home/src/main/res/drawable/ic_android.xml create mode 100644 feature/home/src/main/res/drawable/img_services_empty.xml rename {notifications => feature/home}/src/main/res/drawable/notif_category_feature.xml (100%) rename {notifications => feature/home}/src/main/res/drawable/notif_category_news.xml (100%) rename {notifications => feature/home}/src/main/res/drawable/notif_category_update.xml (100%) rename {notifications => feature/home}/src/main/res/drawable/notif_category_video.xml (100%) create mode 100644 feature/trash/.gitignore create mode 100644 feature/trash/build.gradle.kts create mode 100644 feature/trash/src/main/AndroidManifest.xml create mode 100644 feature/trash/src/main/java/com/twofasapp/feature/trash/di/TrashModule.kt create mode 100644 feature/trash/src/main/java/com/twofasapp/feature/trash/navigation/TrashNavigation.kt create mode 100644 feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeScreen.kt create mode 100644 feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeViewModel.kt create mode 100644 feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashScreen.kt create mode 100644 feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashViewModel.kt delete mode 100644 navigation/src/main/java/com/twofasapp/navigation/ExternalImportDirections.kt delete mode 100644 navigation/src/main/java/com/twofasapp/navigation/ExternalImportRouter.kt delete mode 100644 navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt delete mode 100644 navigation/src/main/java/com/twofasapp/navigation/SettingsRouter.kt delete mode 100644 network/src/main/java/com/twofasapp/network/NetworkModule.kt delete mode 100644 network/src/main/java/com/twofasapp/network/api/BrowserExtensionApi.kt delete mode 100644 network/src/main/java/com/twofasapp/network/api/NotificationsApi.kt delete mode 100644 network/src/main/java/com/twofasapp/network/config/HostVerifier.kt delete mode 100644 network/src/main/java/com/twofasapp/network/config/SslSocketFactory.kt delete mode 100644 network/src/main/java/com/twofasapp/network/config/TrustManagerSelfSigned.kt delete mode 100644 notifications/build.gradle.kts delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/NotificationsModule.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalData.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalDataImpl.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteData.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteDataImpl.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCase.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCaseImpl.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/GetNotificationsCase.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCase.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCaseImpl.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/ObserveNotificationsCase.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/ReadAllNotificationsCase.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepository.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepositoryImpl.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsActivity.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsUiState.kt delete mode 100644 notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsViewModel.kt delete mode 100644 persistence/src/main/java/com/twofasapp/persistence/dao/ServiceDao.kt delete mode 100644 prefs/src/main/java/com/twofasapp/prefs/usecase/MobileDevicePreference.kt create mode 100644 services/domain/src/main/java/com/twofasapp/services/domain/otp/InvalidSecretFormat.kt delete mode 100644 settings/build.gradle.kts delete mode 100644 settings/src/main/AndroidManifest.xml delete mode 100644 settings/src/main/java/com/twofasapp/settings/SettingsModule.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/SettingsActivity.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreen.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreenFactory.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainUiState.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainViewModel.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreen.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreenFactory.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeUiState.kt delete mode 100644 settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeViewModel.kt delete mode 100644 start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingActivity.kt delete mode 100644 start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingViewModel.kt delete mode 100644 start/src/main/java/com/twofasapp/start/ui/onboarding/step/OnboardingStepFragment.kt delete mode 100644 start/src/main/res/layout/activity_onboarding.xml delete mode 100644 start/src/main/res/layout/fragment_onboarding_step.xml diff --git a/about/build.gradle.kts b/about/build.gradle.kts deleted file mode 100644 index 0dcb6c03..00000000 --- a/about/build.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -@Suppress("DSL_SCOPE_VIOLATION") -plugins { - alias(libs.plugins.twofasAndroidLibrary) - alias(libs.plugins.twofasCompose) -} - -android { - namespace = "com.twofasapp.about" -} - -dependencies { - implementation(project(":base")) - implementation(project(":di")) - implementation(project(":resources")) - implementation(project(":extensions")) - implementation(project(":design")) - implementation(project(":environment")) - implementation(project(":featuretoggle")) - implementation(project(":prefs")) - - implementation(libs.bundles.appCompat) - implementation(libs.bundles.compose) - implementation(libs.bundles.playReview) - implementation(libs.webkit) - -} \ No newline at end of file diff --git a/about/src/main/AndroidManifest.xml b/about/src/main/AndroidManifest.xml deleted file mode 100644 index 19b3af70..00000000 --- a/about/src/main/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/about/src/main/java/com/twofasapp/about/ui/AboutActivity.kt b/about/src/main/java/com/twofasapp/about/ui/AboutActivity.kt deleted file mode 100644 index 4dda212a..00000000 --- a/about/src/main/java/com/twofasapp/about/ui/AboutActivity.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.twofasapp.about.ui - -import android.os.Bundle -import android.webkit.WebView -import android.webkit.WebViewClient -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment.Companion.Bottom -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.app.ShareCompat -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.webkit.WebSettingsCompat -import androidx.webkit.WebViewFeature -import com.google.android.play.core.review.ReviewManagerFactory -import com.twofasapp.resources.R -import com.twofasapp.design.compose.HeaderEntry -import com.twofasapp.design.compose.SimpleEntry -import com.twofasapp.design.compose.Toolbar -import com.twofasapp.design.theme.AppThemeLegacy -import com.twofasapp.design.theme.divider -import com.twofasapp.design.theme.isNight -import com.twofasapp.design.theme.textSecondary -import com.twofasapp.extensions.openBrowserApp -import org.koin.androidx.viewmodel.ext.android.viewModel - -class AboutActivity : ComponentActivity() { - - private val viewModel: AboutViewModel by viewModel() - - companion object Screens { - private const val About = "about" - private const val Licenses = "licenses" - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - AppThemeLegacy { - Surface { - val navController = rememberNavController() - - NavHost(navController = navController, startDestination = About) { - composable(About) { About(navController) } - composable(Licenses) { Licenses() } - } - } - } - } - } - - @Composable - private fun About(navController: NavHostController) { - val uiState = viewModel.uiState.collectAsState() - val context = LocalContext.current - - Scaffold( - topBar = { Toolbar(title = stringResource(id = R.string.settings__about)) { onBackPressed() } } - ) { padding -> - Column(modifier = Modifier.padding(padding)) { - - LazyColumn(modifier = Modifier.weight(1f)) { - item { HeaderEntry(text = stringResource(id = R.string.settings__general)) } - item { - SimpleEntry( - title = stringResource(id = R.string.settings__write_a_review), - icon = painterResource(id = R.drawable.ic_about_write_review), - click = { - val manager = ReviewManagerFactory.create(this@AboutActivity) - val request = manager.requestReviewFlow() - request.addOnCompleteListener { task -> - if (task.isSuccessful) { - val flow = manager.launchReviewFlow( - this@AboutActivity, - task.result - ) - flow.addOnCompleteListener { - viewModel.reviewDone() - } - } else { - - } - } - } - ) - } - - item { - SimpleEntry( - title = stringResource(id = R.string.settings__privacy_policy), - icon = painterResource(id = R.drawable.ic_about_privacy_policy), - click = { openBrowserApp(url = "https://2fas.com/privacy-policy/") } - ) - } - - item { - SimpleEntry( - title = stringResource(id = R.string.settings__terms_of_service), - icon = painterResource(id = R.drawable.ic_about_terms), - click = { openBrowserApp(url = "https://2fas.com/terms-of-service/") } - ) - } - - item { - SimpleEntry( - title = stringResource(id = R.string.about_licenses), - icon = painterResource(id = R.drawable.ic_about_licenses), - click = { navController.navigate(Licenses) } - ) - } - - item { Divider(color = MaterialTheme.colors.divider) } - - item { HeaderEntry(text = stringResource(id = R.string.settings__share_app)) } - item { - SimpleEntry( - title = stringResource(id = R.string.settings__tell_a_friend), - icon = painterResource(id = R.drawable.ic_about_share), - click = { - ShareCompat.IntentBuilder(context) - .setType("text/plain") - .setChooserTitle("Share 2FAS") - .setText(getString(R.string.settings__recommendation)) - .startChooser() - } - ) - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = "App version ${uiState.value.versionName}", - color = MaterialTheme.colors.textSecondary, - modifier = Modifier - .weight(1f) - .align(Bottom) - ) - - Image( - painter = painterResource(id = R.drawable.logo_2fas), - contentDescription = null, - modifier = Modifier.size(36.dp) - ) - } - } - } - } - - @Composable - private fun Licenses() { - val isNight = isNight() - - Scaffold( - topBar = { Toolbar(title = stringResource(id = R.string.about_licenses)) { onBackPressed() } } - ) { padding -> - AndroidView(factory = { - WebView(this).apply { - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK) && isNight) { - WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_ON); - } - - webViewClient = WebViewClient() - try { - loadUrl("file:///android_asset/open_source_licenses.html") - } catch (e: Exception) { - Toast.makeText( - context, - "There is no WebView installed. Can not display licenses.", - Toast.LENGTH_LONG - ).show() - finish() - } - } - }) - } - } -} diff --git a/about/src/main/java/com/twofasapp/about/ui/AboutUiState.kt b/about/src/main/java/com/twofasapp/about/ui/AboutUiState.kt deleted file mode 100644 index b1d57df9..00000000 --- a/about/src/main/java/com/twofasapp/about/ui/AboutUiState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.twofasapp.about.ui - -internal data class AboutUiState( - val versionName: String = "", -) diff --git a/about/src/main/java/com/twofasapp/about/ui/AboutViewModel.kt b/about/src/main/java/com/twofasapp/about/ui/AboutViewModel.kt deleted file mode 100644 index fff8a450..00000000 --- a/about/src/main/java/com/twofasapp/about/ui/AboutViewModel.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.twofasapp.about.ui - -import com.twofasapp.base.BaseViewModel -import com.twofasapp.base.dispatcher.Dispatchers -import com.twofasapp.environment.AppConfig -import com.twofasapp.environment.BuildVariant -import com.twofasapp.prefs.usecase.RateAppStatusPreference -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.time.Instant - -internal class AboutViewModel( - private val dispatchers: Dispatchers, - private val appConfig: AppConfig, - private val rateAppStatusPreference: RateAppStatusPreference - -) : BaseViewModel() { - - private val _uiState = MutableStateFlow(AboutUiState()) - val uiState = _uiState.asStateFlow() - - init { - _uiState.update { - it.copy(versionName = generateVersionName()) - } - } - - private fun generateVersionName(): String = - if (appConfig.buildVariant != BuildVariant.Release) { - "${appConfig.versionName} (${appConfig.buildVariant.name})" - } else { - appConfig.versionName - } - - fun reviewDone() { - rateAppStatusPreference.put( - rateAppStatusPreference.get().copy(counterStarted = Instant.now(), counterReached = Instant.now()) - ) - } -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07bfacaa..64a2d7e6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,10 +15,10 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":extensions")) implementation(project(":prefs")) - implementation(project(":environment")) + implementation(project(":serialization")) implementation(project(":time")) implementation(project(":spanner")) @@ -29,17 +29,13 @@ dependencies { implementation(project(":resources")) implementation(project(":design")) implementation(project(":permissions")) - implementation(project(":network")) implementation(project(":push")) implementation(project(":persistence")) implementation(project(":qrscanner")) - implementation(project(":about")) - implementation(project(":settings")) implementation(project(":widgets")) implementation(project(":services")) implementation(project(":services:domain")) implementation(project(":widgets:domain")) - implementation(project(":notifications")) implementation(project(":navigation")) implementation(project(":backup")) implementation(project(":core")) @@ -51,15 +47,22 @@ dependencies { implementation(project(":start")) implementation(project(":start:domain")) implementation(project(":time:domain")) - implementation(project(":externalimport")) - implementation(project(":browserextension:domain")) implementation(project(":core:common")) implementation(project(":core:designsystem")) implementation(project(":core:storage")) + implementation(project(":core:network")) + implementation(project(":data:notifications")) implementation(project(":data:session")) + implementation(project(":data:services")) + implementation(project(":data:browserext")) implementation(project(":feature:startup")) implementation(project(":feature:home")) + implementation(project(":feature:trash")) + implementation(project(":feature:about")) + implementation(project(":feature:externalimport")) + implementation(project(":feature:browserext")) + implementation(project(":feature:appsettings")) implementation(libs.bundles.appCompat) implementation(libs.bundles.rxJava) @@ -71,6 +74,7 @@ dependencies { implementation(libs.bundles.room) implementation(libs.bundles.compose) implementation(libs.bundles.viewModel) + implementation(libs.bundles.accompanist) implementation(libs.bundles.playReview) implementation(libs.bundles.playUpdate) implementation(libs.timber) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ca984ce..a8ccbc8b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,10 +92,10 @@ @@ -164,26 +164,10 @@ - - - - - - - - { scopedOf(::BackupPresenter) { bind() } } - activityScope { - scopedOf(::DisposeServicePresenter) { bind() } - } activityScope { scopedOf(::ExportBackupPresenter) { bind() } } @@ -97,7 +88,4 @@ val activityScopeModule = module { activityScope { scopedOf(::WidgetSettingsPresenter) { bind() } } - activityScope { - scopedOf(::TrashPresenter) { bind() } - } } \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/App.kt b/app/src/main/java/com/twofasapp/App.kt index db55f8d3..e1620858 100644 --- a/app/src/main/java/com/twofasapp/App.kt +++ b/app/src/main/java/com/twofasapp/App.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDex import androidx.multidex.MultiDexApplication -import com.twofasapp.about.AboutModule import com.twofasapp.backup.BackupModule import com.twofasapp.backup.domain.SyncBackupTrigger import com.twofasapp.backup.domain.SyncBackupWorkDispatcher @@ -17,11 +16,8 @@ import com.twofasapp.browserextension.BrowserExtensionModule import com.twofasapp.core.log.FileLogger import com.twofasapp.developer.DeveloperModule import com.twofasapp.di.Modules -import com.twofasapp.externalimport.ExternalImportModule import com.twofasapp.featuretoggle.FeatureToggleModule import com.twofasapp.featuretoggle.domain.FetchRemoteConfigCase -import com.twofasapp.network.NetworkModule -import com.twofasapp.notifications.NotificationsModule import com.twofasapp.parsers.ParsersModule import com.twofasapp.permissions.PermissionsModule import com.twofasapp.persistence.PersistenceModule @@ -35,7 +31,6 @@ import com.twofasapp.services.ServicesModule import com.twofasapp.services.backup.remoteBackupModule import com.twofasapp.services.backupcipher.backupCipherModule import com.twofasapp.services.googleauth.googleAuthModule -import com.twofasapp.settings.SettingsModule import com.twofasapp.start.StartModule import com.twofasapp.time.TimeModule import com.twofasapp.usecases.services.PinOptionsUseCase @@ -80,20 +75,15 @@ class App : MultiDexApplication() { PermissionsModule(), PreferencesPlainModule(), PreferencesEncryptedModule(), - NetworkModule(), BrowserExtensionModule(), PushModule(), PersistenceModule(), QrScannerModule(), - AboutModule(), - SettingsModule(), ServicesModule(), - NotificationsModule(), BackupModule(), FeatureToggleModule(), DeveloperModule(), SecurityModule(), - ExternalImportModule(), ) .map { it.provide() } .plus(Modules.provide()) diff --git a/app/src/main/java/com/twofasapp/AppConfigImpl.kt b/app/src/main/java/com/twofasapp/AppConfigImpl.kt deleted file mode 100644 index 7fbcda67..00000000 --- a/app/src/main/java/com/twofasapp/AppConfigImpl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.twofasapp - -import com.twofasapp.environment.AppConfig -import com.twofasapp.environment.BuildVariant - -class AppConfigImpl : AppConfig { - - override val id: String = BuildConfig.APPLICATION_ID - - override val isDebug: Boolean = BuildConfig.DEBUG - - override val versionName: String = BuildConfig.VERSION_NAME - - override val versionCode: Int = BuildConfig.VERSION_CODE - - override val buildVariant: BuildVariant = when (BuildConfig.BUILD_TYPE) { - "debug" -> BuildVariant.Debug - "releaseLocal" -> BuildVariant.ReleaseLocal - "release" -> BuildVariant.Release - else -> throw RuntimeException("Unknown build variant!") - } - - override val deviceName: String = android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/AppModule.kt b/app/src/main/java/com/twofasapp/AppModule.kt index 70244024..4e309bf6 100644 --- a/app/src/main/java/com/twofasapp/AppModule.kt +++ b/app/src/main/java/com/twofasapp/AppModule.kt @@ -7,8 +7,6 @@ import com.twofasapp.base.dispatcher.AppDispatchers import com.twofasapp.base.dispatcher.Dispatchers import com.twofasapp.core.cipher.CipherService import com.twofasapp.core.cipher.CipherServiceImpl -import com.twofasapp.environment.AppConfig -import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter import com.twofasapp.permissions.CameraPermissionRequest import com.twofasapp.permissions.CameraPermissionRequestFlow import com.twofasapp.services.analytics.AnalyticsServiceFirebase @@ -31,8 +29,6 @@ val applicationModule = module { single { AnalyticsServiceFirebase().apply { init(androidContext()) } } - single { AppConfigImpl() } - single { CipherServiceImpl() } single { com.twofasapp.backup.data.FilesProviderImpl(androidContext()) } diff --git a/app/src/main/java/com/twofasapp/NavigationModule.kt b/app/src/main/java/com/twofasapp/NavigationModule.kt index 84930d85..131cbef0 100644 --- a/app/src/main/java/com/twofasapp/NavigationModule.kt +++ b/app/src/main/java/com/twofasapp/NavigationModule.kt @@ -1,7 +1,12 @@ package com.twofasapp import com.twofasapp.di.KoinModule -import com.twofasapp.navigation.* +import com.twofasapp.navigation.SecurityRouter +import com.twofasapp.navigation.SecurityRouterImpl +import com.twofasapp.navigation.ServiceRouter +import com.twofasapp.navigation.ServiceRouterImpl +import com.twofasapp.navigation.StartRouter +import com.twofasapp.navigation.StartRouterImpl import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf @@ -11,9 +16,7 @@ class NavigationModule : KoinModule { override fun provide() = module { factoryOf(::StartRouterImpl) { bind() } - singleOf(::SettingsRouterImpl) { bind() } singleOf(::ServiceRouterImpl) { bind() } singleOf(::SecurityRouterImpl) { bind() } - singleOf(::ExternalImportRouterImpl) { bind() } } } \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/UseCaseModule.kt b/app/src/main/java/com/twofasapp/UseCaseModule.kt index 32395f67..786facd2 100644 --- a/app/src/main/java/com/twofasapp/UseCaseModule.kt +++ b/app/src/main/java/com/twofasapp/UseCaseModule.kt @@ -32,7 +32,6 @@ import com.twofasapp.usecases.services.EditStateObserver import com.twofasapp.usecases.services.GetService import com.twofasapp.usecases.services.GetServices import com.twofasapp.usecases.services.GetServicesIncludingTrashed -import com.twofasapp.usecases.services.GetTrashedServices import com.twofasapp.usecases.services.PinOptionsUseCase import com.twofasapp.usecases.services.RestoreService import com.twofasapp.usecases.services.SearchStateObserver @@ -74,7 +73,6 @@ val useCaseModule = module { single { GetServices(get()) } single { GetServices(get()) } single { GetServicesIncludingTrashed(get()) } - single { GetTrashedServices(get()) } single { ServicesObserver(get()) } single { GetService(get()) } single { TrashService(get(), get(), get(), get(), get(), get(), get(), get()) } diff --git a/app/src/main/java/com/twofasapp/di/AppModule.kt b/app/src/main/java/com/twofasapp/di/AppModule.kt index ad5bf201..b00d16d0 100644 --- a/app/src/main/java/com/twofasapp/di/AppModule.kt +++ b/app/src/main/java/com/twofasapp/di/AppModule.kt @@ -2,12 +2,27 @@ package com.twofasapp.di import com.twofasapp.analytics.AnalyticsFirebase import com.twofasapp.common.analytics.Analytics +import com.twofasapp.common.environment.AppBuild +import com.twofasapp.common.time.TimeProvider +import com.twofasapp.environment.AppBuildImpl +import com.twofasapp.time.TimeProviderImpl +import kotlinx.serialization.json.Json import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf import org.koin.dsl.module class AppModule : KoinModule { override fun provide() = module { + single { + Json { + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + coerceInputValues = true + } + } singleOf(::AnalyticsFirebase) { bind() } + singleOf(::AppBuildImpl) { bind() } + singleOf(::TimeProviderImpl) { bind() } } } \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/di/Modules.kt b/app/src/main/java/com/twofasapp/di/Modules.kt index 0a84a629..3917f0f3 100644 --- a/app/src/main/java/com/twofasapp/di/Modules.kt +++ b/app/src/main/java/com/twofasapp/di/Modules.kt @@ -1,27 +1,43 @@ package com.twofasapp.di import com.twofasapp.common.di.CommonModule +import com.twofasapp.data.browserext.di.DataBrowserExtModule +import com.twofasapp.data.notifications.di.DataNotificationsModule +import com.twofasapp.data.services.di.DataServicesModule import com.twofasapp.data.session.di.DataSessionModule +import com.twofasapp.feature.about.di.AboutModule +import com.twofasapp.feature.appsettings.di.AppSettingsModule +import com.twofasapp.feature.externalimport.di.ExternalImportModule import com.twofasapp.feature.home.di.HomeModule import com.twofasapp.feature.startup.di.StartupModule -import com.twofasapp.storage.di.PreferencesModule +import com.twofasapp.feature.trash.di.TrashModule +import com.twofasapp.network.di.NetworkModule +import com.twofasapp.storage.di.StorageModule import org.koin.core.module.Module object Modules { private val app = listOf( AppModule(), CommonModule(), - PreferencesModule(), + NetworkModule(), + StorageModule(), ) private val data = listOf( + DataNotificationsModule(), DataSessionModule(), + DataServicesModule(), + DataBrowserExtModule(), ) private val feature = listOf( MainModule(), StartupModule(), HomeModule(), + ExternalImportModule(), + AppSettingsModule(), + TrashModule(), + AboutModule(), ) fun provide(): List = diff --git a/app/src/main/java/com/twofasapp/environment/AppBuildImpl.kt b/app/src/main/java/com/twofasapp/environment/AppBuildImpl.kt new file mode 100644 index 00000000..ad914baf --- /dev/null +++ b/app/src/main/java/com/twofasapp/environment/AppBuildImpl.kt @@ -0,0 +1,27 @@ +package com.twofasapp.environment + +import com.twofasapp.BuildConfig +import com.twofasapp.common.environment.AppBuild +import com.twofasapp.common.environment.BuildVariant +import com.twofasapp.common.environment.BuildVariant.Debug +import com.twofasapp.common.environment.BuildVariant.Release +import com.twofasapp.common.environment.BuildVariant.ReleaseLocal + +class AppBuildImpl : AppBuild { + override val id: String = BuildConfig.APPLICATION_ID + + override val isDebuggable: Boolean = BuildConfig.DEBUG + + override val versionName: String = BuildConfig.VERSION_NAME + + override val versionCode: Int = BuildConfig.VERSION_CODE + + override val buildVariant: BuildVariant = when (BuildConfig.BUILD_TYPE) { + "debug" -> Debug + "releaseLocal" -> ReleaseLocal + "release" -> Release + else -> throw RuntimeException("Unknown build variant!") + } + + override val deviceName: String = android.os.Build.MANUFACTURER + " " + android.os.Build.MODEL +} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/addserviceqr/AddServiceQrPresenter.kt b/app/src/main/java/com/twofasapp/features/addserviceqr/AddServiceQrPresenter.kt index 76b206b9..3fb4136a 100644 --- a/app/src/main/java/com/twofasapp/features/addserviceqr/AddServiceQrPresenter.kt +++ b/app/src/main/java/com/twofasapp/features/addserviceqr/AddServiceQrPresenter.kt @@ -3,19 +3,19 @@ package com.twofasapp.features.addserviceqr import android.net.Uri import com.twofasapp.backup.domain.SyncBackupTrigger import com.twofasapp.backup.domain.SyncBackupWorkDispatcher -import com.twofasapp.environment.AppConfig +import com.twofasapp.common.environment.AppBuild import com.twofasapp.extensions.removeWhiteCharacters -import com.twofasapp.externalimport.domain.ExternalImport -import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter +import com.twofasapp.feature.externalimport.domain.ExternalImport +import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter import com.twofasapp.prefs.ScopedNavigator import com.twofasapp.prefs.model.OtpAuthLink import com.twofasapp.prefs.model.ServiceDto import com.twofasapp.prefs.usecase.LastScannedQrPreference import com.twofasapp.qrscanner.domain.ReadQrFromImageRx import com.twofasapp.qrscanner.domain.ScanQr +import com.twofasapp.services.domain.ConvertOtpLinkToService import com.twofasapp.usecases.services.AddService import com.twofasapp.usecases.services.CheckServiceExists -import com.twofasapp.services.domain.ConvertOtpLinkToService import com.twofasapp.usecases.services.GetService import com.twofasapp.usecases.totp.ParseOtpAuthLink import io.reactivex.rxkotlin.toFlowable @@ -33,7 +33,7 @@ class AddServiceQrPresenter( private val getService: GetService, private val readQrFromImageRx: ReadQrFromImageRx, private val googleAuthenticatorImporter: GoogleAuthenticatorImporter, - private val appConfig: AppConfig, + private val appBuild: AppBuild, private val lastScannedQrPreference: LastScannedQrPreference, ) : AddServiceQrContract.Presenter() { @@ -69,7 +69,7 @@ class AddServiceQrPresenter( isGoogleAuthenticatorLink(content) -> importFromGoogleAuthenticator(isFromGallery, content) isMarketLink(content) -> view.showIncorrectQrStoreLink { resetScanner() } else -> { - if (appConfig.isDebug) { + if (appBuild.isDebuggable) { lastScannedQrPreference.put(content) } @@ -125,6 +125,7 @@ class AddServiceQrPresenter( ) return } + is ExternalImport.ParsingError -> onSaveFailed(isFromGallery, result.reason) ExternalImport.UnsupportedError -> onSaveFailed(isFromGallery, null) } diff --git a/app/src/main/java/com/twofasapp/features/main/MainPresenter.kt b/app/src/main/java/com/twofasapp/features/main/MainPresenter.kt index 1d41a237..505d5949 100644 --- a/app/src/main/java/com/twofasapp/features/main/MainPresenter.kt +++ b/app/src/main/java/com/twofasapp/features/main/MainPresenter.kt @@ -1,10 +1,9 @@ package com.twofasapp.features.main -import com.twofasapp.resources.R import com.twofasapp.backup.domain.SyncBackupTrigger import com.twofasapp.backup.domain.SyncBackupWorkDispatcher +import com.twofasapp.common.environment.AppBuild import com.twofasapp.core.analytics.AnalyticsService -import com.twofasapp.environment.AppConfig import com.twofasapp.extensions.doNothing import com.twofasapp.parsers.ServiceIcons import com.twofasapp.permissions.CameraPermissionRequest @@ -14,6 +13,7 @@ import com.twofasapp.prefs.model.RemoteBackupStatus import com.twofasapp.prefs.model.ServiceDto import com.twofasapp.prefs.usecase.RemoteBackupStatusPreference import com.twofasapp.prefs.usecase.StoreGroups +import com.twofasapp.resources.R import com.twofasapp.services.domain.ConvertOtpLinkToService import com.twofasapp.services.domain.StoreHotpServices import com.twofasapp.start.domain.DeeplinkHandler @@ -42,7 +42,7 @@ class MainPresenter( private val syncSyncBackupDispatcher: SyncBackupWorkDispatcher, private val servicesRefreshTrigger: ServicesRefreshTrigger, private val addService: AddService, - private val appConfig: AppConfig, + private val appBuild: AppBuild, private val parseOtpAuthLink: ParseOtpAuthLink, private val convertOtpLinkToService: ConvertOtpLinkToService, private val deeplinkHandler: DeeplinkHandler, @@ -109,7 +109,7 @@ class MainPresenter( } override fun markAppUpdateDisplayed() { - appUpdateLastCheckVersionPreference.put(appConfig.versionCode.toLong()) + appUpdateLastCheckVersionPreference.put(appBuild.versionCode.toLong()) } override fun updateUnreadNotifications(hasUnreadNotifications: Boolean) { @@ -219,7 +219,7 @@ class MainPresenter( } override fun canDisplayAppUpdate(): Boolean { - return appConfig.versionCode.toLong() != appUpdateLastCheckVersionPreference.get() + return appBuild.versionCode.toLong() != appUpdateLastCheckVersionPreference.get() && rateAppCondition.execute().not() } } diff --git a/app/src/main/java/com/twofasapp/features/main/MainServicesActivity.kt b/app/src/main/java/com/twofasapp/features/main/MainServicesActivity.kt index 536604eb..6fed36f1 100644 --- a/app/src/main/java/com/twofasapp/features/main/MainServicesActivity.kt +++ b/app/src/main/java/com/twofasapp/features/main/MainServicesActivity.kt @@ -27,15 +27,8 @@ import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability import com.google.android.play.core.review.ReviewManagerFactory import com.jakewharton.rxbinding3.appcompat.queryTextChanges -import com.twofasapp.resources.R import com.twofasapp.base.BaseActivityPresenter import com.twofasapp.base.lifecycle.AuthAware -import com.twofasapp.browserextension.domain.FetchTokenRequestsCase -import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase -import com.twofasapp.browserextension.notification.BrowserExtensionRequestPayload -import com.twofasapp.browserextension.notification.BrowserExtensionRequestReceiver -import com.twofasapp.browserextension.notification.DomainMatcher -import com.twofasapp.browserextension.ui.request.BrowserExtensionRequestActivity import com.twofasapp.databinding.ActivityMainBinding import com.twofasapp.design.dialogs.CancelAction import com.twofasapp.design.dialogs.ConfirmAction @@ -53,18 +46,15 @@ import com.twofasapp.features.addserviceqr.AddServiceQrActivity import com.twofasapp.features.addserviceqr.ScanInfoDialog import com.twofasapp.features.backup.BackupActivity import com.twofasapp.features.services.addedservice.AddedServiceBottomSheet -import com.twofasapp.notifications.domain.FetchNotificationsCase -import com.twofasapp.notifications.domain.HasUnreadNotificationsCase import com.twofasapp.permissions.RationaleDialog import com.twofasapp.prefs.model.ServiceDto +import com.twofasapp.resources.R import com.twofasapp.security.ui.security.SecurityActivity import com.twofasapp.services.domain.GetServicesCase import com.twofasapp.services.ui.ServiceActivity import com.twofasapp.usecases.services.EditStateObserver import com.twofasapp.usecases.services.SearchStateObserver import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import lt.neworld.spanner.Spanner import lt.neworld.spanner.Spans @@ -80,10 +70,9 @@ class MainServicesActivity : BaseActivityPresenter(), MainC private val presenter: MainContract.Presenter by injectThis() private val editStateObserver: EditStateObserver by inject() private val searchStateObserver: SearchStateObserver by inject() - private val hasUnreadNotificationsCase: HasUnreadNotificationsCase by inject() - private val fetchNotificationsCase: FetchNotificationsCase by inject() - private val fetchTokenRequestsCase: FetchTokenRequestsCase by inject() - private val observeMobileDeviceCase: ObserveMobileDeviceCase by inject() + + // private val fetchTokenRequestsCase: FetchTokenRequestsCase by inject() +// private val observeMobileDeviceCase: ObserveMobileDeviceCase by inject() private val getServicesCase: GetServicesCase by inject() private val authenticationDialogs = mutableMapOf() private val fabMenuDelegate: FabMenuDelegate by lazy { FabMenuDelegate(this, viewBinding) } @@ -134,115 +123,104 @@ class MainServicesActivity : BaseActivityPresenter(), MainC startActivity() } - lifecycleScope.launch { - lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - hasUnreadNotificationsCase().flowOn(Dispatchers.IO).collect { - presenter.updateUnreadNotifications(it) - } - } - } - } - lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { - launch(Dispatchers.IO) { fetchNotificationsCase() } launch(Dispatchers.IO) { - launch(Dispatchers.IO) { - - try { - val mobileDevice = observeMobileDeviceCase.invoke().first() - val tokenRequests = fetchTokenRequestsCase(mobileDevice.id) - - - tokenRequests.forEach { tokenRequest -> - val domain = DomainMatcher.extractDomain(tokenRequest.domain) - val matchedServices = DomainMatcher.findServicesMatchingDomain( - getServicesCase(), - domain - ) - - runOnUiThread { - if (authenticationDialogs.containsKey(tokenRequest.requestId) - .not() - ) { - authenticationDialogs.put( - tokenRequest.requestId, - MaterialDialog(this@MainServicesActivity) - .title(text = "2FA token request") - .message(text = "Do you want to share the 2FA token to ${tokenRequest.domain}?") - .cancelable(false) - .positiveButton(text = "Approve") { - val isOneDomainMatched = - matchedServices.size == 1 - val serviceId = - if (matchedServices.size == 1) matchedServices.first().id else null - - if (isOneDomainMatched) { - val payload = - BrowserExtensionRequestPayload( - action = BrowserExtensionRequestPayload.Action.Approve, - notificationId = -1, - extensionId = tokenRequest.extensionId, - requestId = tokenRequest.requestId, - serviceId = serviceId ?: -1, - domain = domain, - ) - sendBroadcast( - BrowserExtensionRequestReceiver.createIntent( - this@MainServicesActivity, - payload - ) - ) - } else { - - val contentIntent = Intent( - this@MainServicesActivity, - BrowserExtensionRequestActivity::class.java - ).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - - putExtra( - BrowserExtensionRequestPayload.Key, - BrowserExtensionRequestPayload( - action = BrowserExtensionRequestPayload.Action.Approve, - notificationId = -1, - extensionId = tokenRequest.extensionId, - requestId = tokenRequest.requestId, - serviceId = serviceId ?: -1, - domain = domain, - ) - ) - } - - startActivity(contentIntent) - } - } - .negativeButton(text = "Deny") { - val payload = BrowserExtensionRequestPayload( - action = BrowserExtensionRequestPayload.Action.Deny, - notificationId = -1, - extensionId = tokenRequest.extensionId, - requestId = tokenRequest.requestId, - serviceId = -1, - domain = domain, - ) - sendBroadcast( - BrowserExtensionRequestReceiver.createIntent( - this@MainServicesActivity, - payload - ) - ) - } - .show { } - ) - } - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } +// launch(Dispatchers.IO) { +// +// try { +// val mobileDevice = observeMobileDeviceCase.invoke().first() +// val tokenRequests = fetchTokenRequestsCase(mobileDevice.id) +// +// +// tokenRequests.forEach { tokenRequest -> +// val domain = DomainMatcher.extractDomain(tokenRequest.domain) +// val matchedServices = DomainMatcher.findServicesMatchingDomain( +// getServicesCase(), +// domain +// ) +// +// runOnUiThread { +// if (authenticationDialogs.containsKey(tokenRequest.requestId) +// .not() +// ) { +// authenticationDialogs.put( +// tokenRequest.requestId, +// MaterialDialog(this@MainServicesActivity) +// .title(text = "2FA token request") +// .message(text = "Do you want to share the 2FA token to ${tokenRequest.domain}?") +// .cancelable(false) +// .positiveButton(text = "Approve") { +// val isOneDomainMatched = +// matchedServices.size == 1 +// val serviceId = +// if (matchedServices.size == 1) matchedServices.first().id else null +// +// if (isOneDomainMatched) { +// val payload = +// BrowserExtensionRequestPayload( +// action = BrowserExtensionRequestPayload.Action.Approve, +// notificationId = -1, +// extensionId = tokenRequest.extensionId, +// requestId = tokenRequest.requestId, +// serviceId = serviceId ?: -1, +// domain = domain, +// ) +// sendBroadcast( +// BrowserExtensionRequestReceiver.createIntent( +// this@MainServicesActivity, +// payload +// ) +// ) +// } else { +// +// val contentIntent = Intent( +// this@MainServicesActivity, +// BrowserExtensionRequestActivity::class.java +// ).apply { +// flags = Intent.FLAG_ACTIVITY_SINGLE_TOP +// +// putExtra( +// BrowserExtensionRequestPayload.Key, +// BrowserExtensionRequestPayload( +// action = BrowserExtensionRequestPayload.Action.Approve, +// notificationId = -1, +// extensionId = tokenRequest.extensionId, +// requestId = tokenRequest.requestId, +// serviceId = serviceId ?: -1, +// domain = domain, +// ) +// ) +// } +// +// startActivity(contentIntent) +// } +// } +// .negativeButton(text = "Deny") { +// val payload = BrowserExtensionRequestPayload( +// action = BrowserExtensionRequestPayload.Action.Deny, +// notificationId = -1, +// extensionId = tokenRequest.extensionId, +// requestId = tokenRequest.requestId, +// serviceId = -1, +// domain = domain, +// ) +// sendBroadcast( +// BrowserExtensionRequestReceiver.createIntent( +// this@MainServicesActivity, +// payload +// ) +// ) +// } +// .show { } +// ) +// } +// } +// } +// } catch (e: Exception) { +// e.printStackTrace() +// } +// } } } } diff --git a/app/src/main/java/com/twofasapp/features/navigator/ActivityScopedNavigator.kt b/app/src/main/java/com/twofasapp/features/navigator/ActivityScopedNavigator.kt index b0bdf797..31851c8b 100644 --- a/app/src/main/java/com/twofasapp/features/navigator/ActivityScopedNavigator.kt +++ b/app/src/main/java/com/twofasapp/features/navigator/ActivityScopedNavigator.kt @@ -5,8 +5,6 @@ import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.net.Uri import androidx.core.os.bundleOf -import com.twofasapp.resources.R -import com.twofasapp.about.ui.AboutActivity import com.twofasapp.backup.ui.export.ExportBackupActivity import com.twofasapp.core.RequestCodes import com.twofasapp.developer.ui.DeveloperActivity @@ -15,22 +13,18 @@ import com.twofasapp.extensions.openBrowserApp import com.twofasapp.extensions.startActivity import com.twofasapp.extensions.startActivityForResult import com.twofasapp.extensions.toastLong -import com.twofasapp.externalimport.ui.ExternalImportActivity import com.twofasapp.features.addserviceqr.AddServiceQrActivity import com.twofasapp.features.backup.BackupActivity import com.twofasapp.features.backup.import.ImportBackupActivity import com.twofasapp.features.main.MainServicesActivity -import com.twofasapp.features.trash.TrashActivity -import com.twofasapp.features.trash.delete.DisposeServiceActivity -import com.twofasapp.notifications.ui.NotificationsActivity import com.twofasapp.prefs.ScopedNavigator import com.twofasapp.prefs.model.CheckLockStatus import com.twofasapp.prefs.model.LockMethodEntity import com.twofasapp.prefs.model.ServiceDto +import com.twofasapp.resources.R import com.twofasapp.security.ui.lock.LockActivity import com.twofasapp.security.ui.security.SecurityActivity import com.twofasapp.services.ui.ServiceActivity -import com.twofasapp.settings.ui.SettingsActivity import com.twofasapp.start.ui.start.StartActivity @@ -95,7 +89,6 @@ class ActivityScopedNavigator( } override fun openDisposeService(service: ServiceDto) { - activity.startActivity(DisposeServiceActivity.ARG_SERVICE to service) } override fun openSecurity() { @@ -110,23 +103,21 @@ class ActivityScopedNavigator( } override fun openSettings() { - activity.startActivity() +// activity.startActivity() } override fun openExternalImport() { - activity.startActivity() +// activity.startActivity() } override fun openTrash() { - activity.startActivity() } override fun openAuthenticate(canGoBack: Boolean, requestCode: Int?) { when (checkLockStatus.execute()) { LockMethodEntity.NO_LOCK -> Unit else -> activity.startActivityForResult( - requestCode - ?: RequestCodes.AUTH_REQUEST_CODE, "canGoBack" to canGoBack + requestCode ?: RequestCodes.AUTH_REQUEST_CODE, "canGoBack" to canGoBack ) } } @@ -144,7 +135,7 @@ class ActivityScopedNavigator( } override fun openAbout() { - activity.startActivity() +// activity.startActivity() } override fun openDeveloperOptions() { @@ -163,7 +154,7 @@ class ActivityScopedNavigator( } override fun openNotifications() { - activity.startActivity() + // TO BE REMOVED } private fun resolveIntent(intent: Intent, action: () -> Unit) { diff --git a/app/src/main/java/com/twofasapp/features/services/NoServicesItem.kt b/app/src/main/java/com/twofasapp/features/services/NoServicesItem.kt index 0c7c98a5..298baa2e 100644 --- a/app/src/main/java/com/twofasapp/features/services/NoServicesItem.kt +++ b/app/src/main/java/com/twofasapp/features/services/NoServicesItem.kt @@ -3,10 +3,8 @@ package com.twofasapp.features.services import android.view.LayoutInflater import android.view.ViewGroup import com.mikepenz.fastadapter.binding.AbstractBindingItem -import com.twofasapp.resources.R import com.twofasapp.databinding.ItemNoServicesBinding -import com.twofasapp.extensions.startActivity -import com.twofasapp.externalimport.ui.ExternalImportActivity +import com.twofasapp.resources.R class NoServicesItem : AbstractBindingItem() { @@ -19,9 +17,5 @@ class NoServicesItem : AbstractBindingItem() { override fun bindView(binding: ItemNoServicesBinding, payloads: List) { super.bindView(binding, payloads) - - binding.externalImport.setOnClickListener { - binding.root.context.startActivity() - } } } \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/EmptyTrashItem.kt b/app/src/main/java/com/twofasapp/features/trash/EmptyTrashItem.kt deleted file mode 100644 index 80bc1cf6..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/EmptyTrashItem.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.twofasapp.features.trash - -import android.view.LayoutInflater -import android.view.ViewGroup -import com.mikepenz.fastadapter.binding.AbstractBindingItem -import com.twofasapp.resources.R -import com.twofasapp.databinding.ItemEmptyTrashBinding - -class EmptyTrashItem : AbstractBindingItem() { - - override var identifier = R.id.item_empty_trash.toLong() - override val type = R.id.item_empty_trash - override var isSelectable = false - - override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) = - ItemEmptyTrashBinding.inflate(inflater, parent, false) -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/TrashActivity.kt b/app/src/main/java/com/twofasapp/features/trash/TrashActivity.kt deleted file mode 100644 index 7ef417b4..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/TrashActivity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.twofasapp.features.trash - -import android.os.Bundle -import androidx.recyclerview.widget.DefaultItemAnimator -import com.mikepenz.fastadapter.IItem -import com.mikepenz.fastadapter.adapters.FastItemAdapter -import com.mikepenz.fastadapter.diff.FastAdapterDiffUtil -import com.twofasapp.base.BaseActivityPresenter -import com.twofasapp.extensions.navigationClicksThrottled -import com.twofasapp.databinding.ActivityTrashBinding -import com.twofasapp.views.ModelDiffUtilCallback - -class TrashActivity : BaseActivityPresenter(), TrashContract.View { - - private val presenter: TrashContract.Presenter by injectThis() - private val adapter = FastItemAdapter>() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivityTrashBinding::inflate) - setPresenter(presenter) - viewBinding.recycler.adapter = adapter - viewBinding.recycler.itemAnimator = DefaultItemAnimator() - } - - override fun toolbarBackClicks() = viewBinding.toolbar.navigationClicksThrottled() - - override fun setItems(items: List>) { - FastAdapterDiffUtil.set(adapter.itemAdapter, items, ModelDiffUtilCallback()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/TrashContract.kt b/app/src/main/java/com/twofasapp/features/trash/TrashContract.kt deleted file mode 100644 index 8ef3306e..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/TrashContract.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.twofasapp.features.trash - -import com.mikepenz.fastadapter.IItem -import com.twofasapp.base.BasePresenter -import io.reactivex.Flowable - -interface TrashContract { - - interface View { - fun toolbarBackClicks(): Flowable - - fun setItems(items: List>) - } - - abstract class Presenter : com.twofasapp.base.BasePresenter() { - } -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/TrashPresenter.kt b/app/src/main/java/com/twofasapp/features/trash/TrashPresenter.kt deleted file mode 100644 index 9c126d64..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/TrashPresenter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.twofasapp.features.trash - -import com.twofasapp.prefs.ScopedNavigator -import com.twofasapp.usecases.services.GetTrashedServices -import com.twofasapp.usecases.services.RestoreService - -class TrashPresenter( - private val view: TrashContract.View, - private val navigator: ScopedNavigator, - private val getTrashedServices: GetTrashedServices, - private val restoreService: RestoreService, -) : TrashContract.Presenter() { - - override fun onViewAttached() { - view.toolbarBackClicks().safelySubscribe { navigator.navigateBack() } - refreshItems() - } - - override fun onResume() { - refreshItems() - } - - private fun refreshItems() { - getTrashedServices.execute() - .safelySubscribe { list -> - - if (list.isEmpty()) { - view.setItems(listOf(EmptyTrashItem())) - return@safelySubscribe - } - - val items = list - .sortedByDescending { it.updatedAt } - .map { service -> - TrashedServiceItem( - model = TrashedService(service), - onRestoreClick = { onRestoreClick(it) }, - onDeleteClick = { onDeleteClick(it) }, - ) - } - - view.setItems(items) - } - } - - private fun onRestoreClick(model: TrashedService) { - restoreService.execute(model.service) - .safelySubscribe { refreshItems() } - } - - private fun onDeleteClick(model: TrashedService) { - navigator.openDisposeService(model.service) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/TrashedService.kt b/app/src/main/java/com/twofasapp/features/trash/TrashedService.kt deleted file mode 100644 index 09259ae3..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/TrashedService.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.twofasapp.features.trash - -import com.twofasapp.prefs.model.ServiceDto - -data class TrashedService( - val service: ServiceDto, -) \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/TrashedServiceItem.kt b/app/src/main/java/com/twofasapp/features/trash/TrashedServiceItem.kt deleted file mode 100644 index 82ea6360..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/TrashedServiceItem.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.twofasapp.features.trash - -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.ViewGroup -import android.widget.PopupMenu -import androidx.core.view.isVisible -import com.mikepenz.fastadapter.binding.ModelAbstractBindingItem -import com.twofasapp.resources.R -import com.twofasapp.databinding.ItemTrashedServiceBinding - -class TrashedServiceItem( - model: TrashedService, - private val onRestoreClick: (TrashedService) -> Unit, - private val onDeleteClick: (TrashedService) -> Unit, -) : ModelAbstractBindingItem(model) { - - override var identifier = model.service.id - override val type = R.id.item_trashed_service - override var isSelectable = false - - override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?) = - ItemTrashedServiceBinding.inflate(inflater, parent, false) - - override fun bindView(binding: ItemTrashedServiceBinding, payloads: List) { - binding.name.text = model.service.name - binding.info.text = model.service.otpAccount - binding.info.isVisible = model.service.otpAccount.isNullOrBlank().not() - - binding.iconLayout.updateIcon(model.service) - - binding.actionMore.setOnClickListener { - val popup = PopupMenu(it.context!!, it) - popup.menuInflater.inflate(com.twofasapp.R.menu.menu_trashed_service, popup.menu) - popup.setOnMenuItemClickListener { menuItem: MenuItem -> - when (menuItem.itemId) { - com.twofasapp.R.id.menu_restore -> onRestoreClick.invoke(model) - com.twofasapp. R.id.menu_delete -> onDeleteClick.invoke(model) - } - true - } - popup.show() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceActivity.kt b/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceActivity.kt deleted file mode 100644 index 6fe495bd..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceActivity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.twofasapp.features.trash.delete - -import android.os.Bundle -import android.widget.TextView -import com.jakewharton.rxbinding3.widget.checkedChanges -import com.twofasapp.base.BaseActivityPresenter -import com.twofasapp.extensions.clicksThrottled -import com.twofasapp.databinding.ActivityDisposeServiceBinding -import com.twofasapp.prefs.model.ServiceDto -import com.twofasapp.services.ui.ServiceActivity -import io.reactivex.BackpressureStrategy -import io.reactivex.Flowable - -class DisposeServiceActivity : BaseActivityPresenter(), DisposeServiceContract.View { - - companion object { - const val ARG_SERVICE = "service" - } - - private val presenter: DisposeServiceContract.Presenter by injectThis() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivityDisposeServiceBinding::inflate) - setPresenter(presenter) - } - - override fun deleteClicks() = viewBinding.delete.clicksThrottled() - - override fun cancelClicks() = viewBinding.cancel.clicksThrottled() - - override fun closeClicks() = viewBinding.close.clicksThrottled() - - override fun deleteSwitchChanges(): Flowable = - viewBinding.deleteSwitch.checkedChanges().toFlowable(BackpressureStrategy.LATEST) - - override fun setDeleteEnabled(isEnabled: Boolean) { - viewBinding.delete.isEnabled = isEnabled - } - - override fun getServiceExtra() = - intent.extras!!.getParcelable(ServiceActivity.ARG_SERVICE)!! - - override fun setHeader(serviceName: String?) { - viewBinding.header.text = serviceName - } - - override fun setNote(resId: Int?, serviceName: String?) = - formatText(viewBinding.note, resId, serviceName) - - private fun formatText(textView: TextView, resId: Int?, serviceName: String?) { - if (resId != null) { - textView.text = getString(resId, serviceName, serviceName) - } else { - textView.text = serviceName - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceContract.kt b/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceContract.kt deleted file mode 100644 index 98545e99..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServiceContract.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.twofasapp.features.trash.delete - -import com.twofasapp.prefs.model.ServiceDto -import io.reactivex.Flowable - -interface DisposeServiceContract { - - interface View { - fun deleteClicks(): Flowable - fun cancelClicks(): Flowable - fun closeClicks(): Flowable - fun deleteSwitchChanges(): Flowable - - fun getServiceExtra(): ServiceDto - fun setHeader(serviceName: String?) - fun setNote(resId: Int?, serviceName: String?) - fun setDeleteEnabled(isEnabled: Boolean) - } - - abstract class Presenter : com.twofasapp.base.BasePresenter() -} diff --git a/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServicePresenter.kt b/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServicePresenter.kt deleted file mode 100644 index e6a40203..00000000 --- a/app/src/main/java/com/twofasapp/features/trash/delete/DisposeServicePresenter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.twofasapp.features.trash.delete - -import com.twofasapp.resources.R -import com.twofasapp.prefs.ScopedNavigator -import com.twofasapp.services.domain.DeleteServiceUseCase - -class DisposeServicePresenter( - private val view: DisposeServiceContract.View, - private val navigator: ScopedNavigator, - private val deleteServiceUseCase: DeleteServiceUseCase, -) : DisposeServiceContract.Presenter() { - - override fun onViewAttached() { - view.getServiceExtra().let { - view.setHeader(it.name) - view.setNote(R.string.tokens__you_will_not_be_able_to_sign_in_to_your, it.name) - } - - view.closeClicks().safelySubscribe { navigator.navigateBack() } - view.cancelClicks().safelySubscribe { navigator.navigateBack() } - view.deleteSwitchChanges().safelySubscribe { view.setDeleteEnabled(it) } - - view.deleteClicks() - .flatMapSingle { deleteServiceUseCase.execute(view.getServiceExtra()).toSingle { } } - .safelySubscribe { - navigator.finish() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/navigation/ExternalImportRouterImpl.kt b/app/src/main/java/com/twofasapp/navigation/ExternalImportRouterImpl.kt deleted file mode 100644 index 0d85dd5d..00000000 --- a/app/src/main/java/com/twofasapp/navigation/ExternalImportRouterImpl.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.twofasapp.navigation - -import androidx.lifecycle.ViewModelStoreOwner -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavHostController -import androidx.navigation.compose.composable -import com.twofasapp.externalimport.ui.aegis.AegisScreenFactory -import com.twofasapp.externalimport.ui.googleauthenticator.GoogleAuthenticatorScreenFactory -import com.twofasapp.externalimport.ui.main.ExternalImportScreenFactory -import com.twofasapp.externalimport.ui.raivo.RaivoScreenFactory -import com.twofasapp.externalimport.ui.result.ImportResultScreenFactory -import com.twofasapp.externalimport.ui.scan.ImportScanScreenFactory -import timber.log.Timber - -class ExternalImportRouterImpl( - private val externalImportScreenFactory: ExternalImportScreenFactory, - private val importScanScreenFactory: ImportScanScreenFactory, - private val importResultScreenFactory: ImportResultScreenFactory, - private val googleAuthenticatorScreenFactory: GoogleAuthenticatorScreenFactory, - private val aegisScreenFactory: AegisScreenFactory, - private val raivoScreenFactory: RaivoScreenFactory, -) : ExternalImportRouter() { - - companion object { - private const val ARG_CONTENT = "ARG_CONTENT" - private const val ARG_TYPE = "ARG_TYPE" - private const val ARG_START_GALLERY = "ARG_START_GALLERY" - - private const val MAIN = "external_import" - private const val IMPORT_SCAN = "import_scan/{$ARG_START_GALLERY}" - private const val IMPORT_RESULT = "import_result/{$ARG_TYPE}/{$ARG_CONTENT}" - private const val GOOGLE_AUTH = "import_google_authenticator" - private const val AEGIS = "import_aegis" - private const val RAIVO = "import_raivo" - } - - override fun buildNavGraph(builder: NavGraphBuilder, viewModelStoreOwner: ViewModelStoreOwner?) { - builder.composable(route = MAIN, content = { externalImportScreenFactory.create() }) - builder.composable( - route = IMPORT_SCAN, - content = { importScanScreenFactory.create(it.arguments?.getString(ARG_START_GALLERY).toBoolean()) }) - builder.composable( - route = IMPORT_RESULT, - content = { - importResultScreenFactory.create( - type = it.arguments?.getString(ARG_TYPE).orEmpty(), - content = it.arguments?.getString(ARG_CONTENT).orEmpty() - ) - } - ) - builder.composable(route = GOOGLE_AUTH, content = { googleAuthenticatorScreenFactory.create() }) - builder.composable(route = AEGIS, content = { aegisScreenFactory.create() }) - builder.composable(route = RAIVO, content = { raivoScreenFactory.create() }) - } - - override fun navigate(navController: NavHostController, direction: ExternalImportDirections) { - Timber.d("$direction") - - when (direction) { - ExternalImportDirections.GoBack -> navController.popBackStack() - ExternalImportDirections.Main -> navController.navigate(MAIN) - is ExternalImportDirections.ImportScan -> { - navController.navigate( - IMPORT_SCAN.replace("{${ARG_START_GALLERY}}", direction.startWithGallery.toString()) - ) - } - is ExternalImportDirections.ImportResult -> { - navController.navigate( - IMPORT_RESULT - .replace("{${ARG_TYPE}}", direction.type.name) - .replace("{${ARG_CONTENT}}", direction.content) - ) { popUpTo(MAIN) } - } - ExternalImportDirections.GoogleAuthenticator -> navController.navigate(GOOGLE_AUTH) - ExternalImportDirections.Aegis -> navController.navigate(AEGIS) - ExternalImportDirections.Raivo -> navController.navigate(RAIVO) - } - } - - override fun navigateBack() { - navigate(ExternalImportDirections.GoBack) - } - - override fun startDirection(): String = MAIN -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt b/app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt deleted file mode 100644 index ca7f0af0..00000000 --- a/app/src/main/java/com/twofasapp/navigation/SettingsRouterImpl.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.twofasapp.navigation - -import androidx.lifecycle.ViewModelStoreOwner -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavHostController -import androidx.navigation.compose.composable -import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory -import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory -import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory -import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory -import com.twofasapp.settings.ui.main.SettingsMainScreenFactory -import com.twofasapp.settings.ui.theme.ThemeScreenFactory -import timber.log.Timber - -class SettingsRouterImpl( - private val settingsMainScreenFactory: SettingsMainScreenFactory, - private val themeScreenFactory: ThemeScreenFactory, - private val browserExtensionScreenFactory: BrowserExtensionScreenFactory, - private val pairingProgressScreenFactory: PairingProgressScreenFactory, - private val pairingScanScreenFactory: PairingScanScreenFactory, - private val browserDetailsScreenFactory: BrowserDetailsScreenFactory, -) : SettingsRouter() { - - companion object { - private const val ARG_EXTENSION_ID = "extensionId" - - private const val MAIN = "settings_main" - private const val THEME = "theme" - private const val BROWSER_EXTENSION = "browser_extension" - private const val BROWSER_DETAILS = "browser_details/{$ARG_EXTENSION_ID}" - private const val PAIRING_SCAN = "pairing_scan" - private const val PAIRING_PROGRESS = "pairing_progress/{$ARG_EXTENSION_ID}" - } - - override fun buildNavGraph(builder: NavGraphBuilder, viewModelStoreOwner: ViewModelStoreOwner?) { - builder.composable(route = MAIN, content = { settingsMainScreenFactory.create() }) - builder.composable(route = THEME, content = { themeScreenFactory.create() }) - builder.composable(route = BROWSER_EXTENSION, content = { browserExtensionScreenFactory.create() }) - builder.composable(route = BROWSER_DETAILS, content = { - browserDetailsScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty()) - }) - builder.composable(route = PAIRING_SCAN, content = { pairingScanScreenFactory.create() }) - builder.composable(route = PAIRING_PROGRESS, content = { - pairingProgressScreenFactory.create(it.arguments?.getString(ARG_EXTENSION_ID).orEmpty()) - }) - } - - override fun startDirection(): String = MAIN - - override fun navigate( - navController: NavHostController, - direction: SettingsDirections, - ) { - Timber.d("$direction") - - when (direction) { - SettingsDirections.GoBack -> navController.popBackStack() - SettingsDirections.Main -> navController.navigate(MAIN) - SettingsDirections.Theme -> navController.navigate(THEME) - SettingsDirections.BrowserExtension -> navController.navigate(BROWSER_EXTENSION) - SettingsDirections.PairingScan -> navController.navigate(PAIRING_SCAN) { popUpTo(BROWSER_EXTENSION) } - - is SettingsDirections.PairingProgress -> navController.navigate( - PAIRING_PROGRESS.replace("{$ARG_EXTENSION_ID}", direction.extensionId) - ) { popUpTo(BROWSER_EXTENSION) } - - is SettingsDirections.BrowserDetails -> navController.navigate( - BROWSER_DETAILS.replace("{$ARG_EXTENSION_ID}", direction.extensionId) - ) - } - } - - override fun navigateBack() { - navigate(SettingsDirections.GoBack) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/navigation/StartRouterImpl.kt b/app/src/main/java/com/twofasapp/navigation/StartRouterImpl.kt index b349048f..be87ae1d 100644 --- a/app/src/main/java/com/twofasapp/navigation/StartRouterImpl.kt +++ b/app/src/main/java/com/twofasapp/navigation/StartRouterImpl.kt @@ -3,7 +3,6 @@ package com.twofasapp.navigation import android.app.Activity import com.twofasapp.extensions.startActivity import com.twofasapp.features.main.MainServicesActivity -import com.twofasapp.start.ui.onboarding.OnboardingActivity class StartRouterImpl( private val activity: Activity @@ -14,7 +13,6 @@ class StartRouterImpl( override fun navigate(direction: StartDirections) { when (direction) { is StartDirections.Main -> activity.startActivity() - is StartDirections.Onboarding -> activity.startActivity() } } } \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/services/backup/usecases/UpdateRemoteBackup.kt b/app/src/main/java/com/twofasapp/services/backup/usecases/UpdateRemoteBackup.kt index f77aab37..2cab707c 100644 --- a/app/src/main/java/com/twofasapp/services/backup/usecases/UpdateRemoteBackup.kt +++ b/app/src/main/java/com/twofasapp/services/backup/usecases/UpdateRemoteBackup.kt @@ -8,7 +8,7 @@ import com.twofasapp.services.googledrive.models.UpdateGoogleDriveFileResult import com.twofasapp.services.googledrive.models.mapToRemoteBackupErrorType import com.twofasapp.prefs.model.isSet import com.twofasapp.base.usecase.UseCaseParameterized -import com.twofasapp.environment.AppConfig +import com.twofasapp.common.environment.AppBuild import com.twofasapp.usecases.services.GetServices import com.twofasapp.prefs.usecase.StoreGroups import com.twofasapp.services.domain.StoreServicesOrder @@ -24,7 +24,7 @@ class UpdateRemoteBackup( private val remoteBackupKeyPreference: com.twofasapp.prefs.usecase.RemoteBackupKeyPreference, private val jsonSerializer: com.twofasapp.serialization.JsonSerializer, private val encryptBackup: EncryptBackup, - private val appConfig: AppConfig, + private val appBuild: AppBuild, ) : UseCaseParameterized> { data class Params( @@ -50,8 +50,8 @@ class UpdateRemoteBackup( com.twofasapp.prefs.model.RemoteBackup( updatedAt = params.updatedAt, - appVersionCode = appConfig.versionCode, - appVersionName = appConfig.versionName, + appVersionCode = appBuild.versionCode, + appVersionName = appBuild.versionName, groups = groups.filter { it.id != null }.map { it.toRemote() }, services = servicesOrdered, account = remoteBackupStatusPreference.get().account, diff --git a/app/src/main/java/com/twofasapp/time/TimeProviderImpl.kt b/app/src/main/java/com/twofasapp/time/TimeProviderImpl.kt new file mode 100644 index 00000000..d87f4c2b --- /dev/null +++ b/app/src/main/java/com/twofasapp/time/TimeProviderImpl.kt @@ -0,0 +1,21 @@ +package com.twofasapp.time + +import android.os.SystemClock +import com.twofasapp.common.time.TimeProvider +import java.time.OffsetDateTime +import java.time.ZoneOffset + +class TimeProviderImpl : TimeProvider { + + override fun currentDateTimeUtc(): OffsetDateTime { + return OffsetDateTime.now(ZoneOffset.UTC) + } + + override fun systemCurrentTime(): Long { + return System.currentTimeMillis() + } + + override fun systemElapsedTime(): Long { + return SystemClock.elapsedRealtime() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/ui/main/MainNavHost.kt b/app/src/main/java/com/twofasapp/ui/main/MainNavHost.kt index 9a51dd74..94db1176 100644 --- a/app/src/main/java/com/twofasapp/ui/main/MainNavHost.kt +++ b/app/src/main/java/com/twofasapp/ui/main/MainNavHost.kt @@ -1,12 +1,30 @@ package com.twofasapp.ui.main +import android.app.Activity import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost +import com.twofasapp.common.ktx.clearGraphBackStack +import com.twofasapp.extensions.startActivity +import com.twofasapp.extensions.startActivityForResult +import com.twofasapp.feature.about.navigation.AboutGraph +import com.twofasapp.feature.about.navigation.aboutNavigation +import com.twofasapp.feature.appsettings.navigation.AppSettingsGraph +import com.twofasapp.feature.appsettings.navigation.appSettingsNavigation +import com.twofasapp.feature.browserext.notification.BrowserExtGraph +import com.twofasapp.feature.browserext.notification.browserExtNavigation +import com.twofasapp.feature.externalimport.navigation.ExternalImportGraph +import com.twofasapp.feature.externalimport.navigation.externalImportNavigation import com.twofasapp.feature.home.navigation.HomeGraph +import com.twofasapp.feature.home.navigation.HomeNavigationListener import com.twofasapp.feature.home.navigation.homeNavigation import com.twofasapp.feature.startup.navigation.startupNavigation - +import com.twofasapp.feature.trash.navigation.TrashGraph +import com.twofasapp.feature.trash.navigation.trashNavigation +import com.twofasapp.features.addserviceqr.AddServiceQrActivity +import com.twofasapp.features.backup.BackupActivity +import com.twofasapp.security.ui.security.SecurityActivity +import com.twofasapp.services.ui.ServiceActivity @Composable fun MainNavHost( @@ -14,10 +32,67 @@ fun MainNavHost( startDestination: String, ) { NavHost(navController = navController, startDestination = startDestination) { + startupNavigation( - onFinish = { navController.navigate(HomeGraph.route) { popUpTo(0) } } + openHome = { navController.navigate(HomeGraph.route) { popUpTo(0) } } ) - homeNavigation() + homeNavigation( + navController = navController, + listener = object : HomeNavigationListener { + override fun openAddManuallyService(activity: Activity) { + activity.startActivity() + } + + override fun openAddQrService(activity: Activity) { + activity.startActivity() + } + + override fun openService(activity: Activity, serviceId: Long) { + activity.startActivityForResult( + ServiceActivity.REQUEST_KEY_ADD_SERVICE, + ServiceActivity.ARG_SERVICE_ID to serviceId, + ) + } + + override fun openExternalImport() { + navController.navigate(ExternalImportGraph.route) + } + + override fun openBrowserExt() { + navController.navigate(BrowserExtGraph.route) + } + + override fun openSecurity(activity: Activity) { + activity.startActivity() + } + + override fun openBackup(activity: Activity) { + activity.startActivity() + } + + override fun openAppSettings() { + navController.navigate(AppSettingsGraph.route) + } + + override fun openTrash() { + navController.navigate(TrashGraph.route) + } + + override fun openAbout() { + navController.navigate(AboutGraph.route) + } + } + ) + + externalImportNavigation( + navController = navController, + onFinish = { navController.clearGraphBackStack() } + ) + + appSettingsNavigation() + trashNavigation(navController = navController) + aboutNavigation(navController = navController) + browserExtNavigation(navController = navController) } } \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/ui/main/MainScreen.kt b/app/src/main/java/com/twofasapp/ui/main/MainScreen.kt index c3848cb5..54d470c5 100644 --- a/app/src/main/java/com/twofasapp/ui/main/MainScreen.kt +++ b/app/src/main/java/com/twofasapp/ui/main/MainScreen.kt @@ -9,7 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.twofasapp.designsystem.MainAppTheme -import com.twofasapp.designsystem.TwsTheme +import com.twofasapp.designsystem.TwTheme import com.twofasapp.feature.home.navigation.HomeGraph import com.twofasapp.feature.startup.navigation.StartupGraph import org.koin.androidx.compose.koinViewModel @@ -24,7 +24,7 @@ fun MainScreen( MainAppTheme { Surface( modifier = Modifier.fillMaxSize(), - color = TwsTheme.color.background, + color = TwTheme.color.background, ) { uiState?.let { val startDestination = when (it) { diff --git a/app/src/main/java/com/twofasapp/ui/main/MainViewModel.kt b/app/src/main/java/com/twofasapp/ui/main/MainViewModel.kt index c3975aa7..a851fca8 100644 --- a/app/src/main/java/com/twofasapp/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/twofasapp/ui/main/MainViewModel.kt @@ -3,6 +3,8 @@ package com.twofasapp.ui.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.twofasapp.common.coroutines.Dispatchers +import com.twofasapp.common.ktx.runSafely +import com.twofasapp.data.notifications.NotificationsRepository import com.twofasapp.data.session.SessionRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -11,6 +13,7 @@ import kotlinx.coroutines.launch class MainViewModel( private val dispatchers: Dispatchers, private val sessionRepository: SessionRepository, + private val notificationsRepository: NotificationsRepository, ) : ViewModel() { val uiState: MutableStateFlow = MutableStateFlow(null) @@ -24,5 +27,9 @@ class MainViewModel( uiState.update { state } } + + viewModelScope.launch { + runSafely { notificationsRepository.fetchNotifications() } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/twofasapp/usecases/backup/SyncBackupServices.kt b/app/src/main/java/com/twofasapp/usecases/backup/SyncBackupServices.kt index d4711d35..43158e88 100644 --- a/app/src/main/java/com/twofasapp/usecases/backup/SyncBackupServices.kt +++ b/app/src/main/java/com/twofasapp/usecases/backup/SyncBackupServices.kt @@ -1,13 +1,12 @@ package com.twofasapp.usecases.backup import android.annotation.SuppressLint -import com.twofasapp.BuildConfig import com.twofasapp.backup.domain.SyncBackupTrigger import com.twofasapp.base.usecase.UseCaseParameterized +import com.twofasapp.common.environment.AppBuild import com.twofasapp.core.analytics.AnalyticsEvent import com.twofasapp.core.analytics.AnalyticsParam import com.twofasapp.entity.SyncBackupResult -import com.twofasapp.environment.AppConfig import com.twofasapp.extensions.doNothing import com.twofasapp.parsers.LegacyTypeToId import com.twofasapp.parsers.ServiceIcons @@ -53,7 +52,7 @@ class SyncBackupServices( private val analyticsService: com.twofasapp.core.analytics.AnalyticsService, private val storeRecentlyDeleted: StoreRecentlyDeleted, private val observeSyncStatus: ObserveSyncStatus, - private val appConfig: AppConfig, + private val appBuild: AppBuild, ) : UseCaseParameterized> { data class Params( @@ -192,7 +191,7 @@ class SyncBackupServices( val localSchemaVersion = backupStatus.schemaVersion val remoteSchemaVersion = remoteStatus.schemaVersion - val localAppVersionCode = appConfig.versionCode + val localAppVersionCode = appBuild.versionCode val remoteAppVersionCode = remoteStatus.appVersionCode // Sync matching groups diff --git a/app/src/main/java/com/twofasapp/usecases/services/GetTrashedServices.kt b/app/src/main/java/com/twofasapp/usecases/services/GetTrashedServices.kt deleted file mode 100644 index 8150b95a..00000000 --- a/app/src/main/java/com/twofasapp/usecases/services/GetTrashedServices.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.twofasapp.usecases.services - -import com.twofasapp.prefs.model.ServiceDto -import com.twofasapp.services.data.ServicesRepository -import com.twofasapp.base.usecase.UseCase -import io.reactivex.Scheduler -import io.reactivex.Single - -class GetTrashedServices(private val servicesRepository: ServicesRepository) : UseCase>> { - - override fun execute(subscribeScheduler: Scheduler, observeScheduler: Scheduler): Single> { - return servicesRepository.select() - .map { list -> list.filter { it.isDeleted == true } } - .subscribeOn(subscribeScheduler) - .observeOn(observeScheduler) - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_trash.xml b/app/src/main/res/layout/activity_trash.xml deleted file mode 100644 index bcea7e58..00000000 --- a/app/src/main/res/layout/activity_trash.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_no_services.xml b/app/src/main/res/layout/item_no_services.xml index 6f6fe57c..617c4615 100644 --- a/app/src/main/res/layout/item_no_services.xml +++ b/app/src/main/res/layout/item_no_services.xml @@ -11,7 +11,7 @@ android:id="@+id/orbits" android:layout_width="110dp" android:layout_height="110dp" - android:src="@drawable/services_empty" /> + android:src="@drawable/img_services_empty" /> - - - - diff --git a/backup/build.gradle.kts b/backup/build.gradle.kts index 58aae977..218e379f 100644 --- a/backup/build.gradle.kts +++ b/backup/build.gradle.kts @@ -12,16 +12,15 @@ android { dependencies { implementation(project(":base")) implementation(project(":core")) - implementation(project(":di")) + implementation(project(":core:common")) + implementation(project(":core:di")) implementation(project(":design")) implementation(project(":extensions")) implementation(project(":permissions")) implementation(project(":prefs")) implementation(project(":persistence")) - implementation(project(":network")) implementation(project(":push")) implementation(project(":resources")) - implementation(project(":environment")) implementation(project(":navigation")) implementation(project(":services:domain")) implementation(project(":serialization")) diff --git a/backup/domain/build.gradle.kts b/backup/domain/build.gradle.kts index be47ba39..f2a587df 100644 --- a/backup/domain/build.gradle.kts +++ b/backup/domain/build.gradle.kts @@ -9,6 +9,6 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":extensions")) } diff --git a/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupSuspended.kt b/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupSuspended.kt index e80fefd1..22312bef 100644 --- a/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupSuspended.kt +++ b/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupSuspended.kt @@ -5,7 +5,7 @@ import android.net.Uri import com.twofasapp.backup.EncryptBackup import com.twofasapp.backup.domain.converter.toRemoteGroup import com.twofasapp.backup.domain.converter.toRemoteService -import com.twofasapp.environment.AppConfig +import com.twofasapp.common.environment.AppBuild import com.twofasapp.prefs.model.RemoteBackup import com.twofasapp.prefs.model.RemoteService import com.twofasapp.serialization.JsonSerializer @@ -17,7 +17,7 @@ import com.twofasapp.time.domain.TimeProvider class ExportBackupSuspended( private val context: Context, private val timeProvider: TimeProvider, - private val appConfig: AppConfig, + private val appBuild: AppBuild, private val servicesRepository: ServicesRepository, private val getServicesCase: GetServicesCase, private val getGroupsCase: GetGroupsCase, @@ -46,8 +46,8 @@ class ExportBackupSuspended( .copy(order = RemoteService.Order(position = servicesOrder.ids.indexOf(it.id))) }, updatedAt = timeProvider.systemCurrentTime(), - appVersionCode = appConfig.versionCode, - appVersionName = appConfig.versionName, + appVersionCode = appBuild.versionCode, + appVersionName = appBuild.versionName, groups = groups.list.filter { group -> group.id != null }.map { group -> group.toRemoteGroup() }, account = null, ) @@ -72,6 +72,7 @@ class ExportBackupSuspended( Result.Success(json) } + is EncryptBackup.Result.Error -> Result.Error(result.throwable) } } diff --git a/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupToDisk.kt b/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupToDisk.kt index cb87d63e..bab24c7f 100644 --- a/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupToDisk.kt +++ b/backup/src/main/java/com/twofasapp/backup/domain/ExportBackupToDisk.kt @@ -4,7 +4,7 @@ import android.content.Context import android.net.Uri import com.twofasapp.backup.EncryptBackup import com.twofasapp.backup.ui.export.ExportBackup -import com.twofasapp.environment.AppConfig +import com.twofasapp.common.environment.AppBuild import com.twofasapp.prefs.usecase.ServicesOrderPreference import com.twofasapp.prefs.usecase.StoreGroups import com.twofasapp.services.domain.GetServicesUseCase @@ -16,7 +16,7 @@ import io.reactivex.schedulers.Schedulers class ExportBackupToDisk( private val context: Context, - private val appConfig: AppConfig, + private val appBuild: AppBuild, private val getServices: GetServicesUseCase, private val timeProvider: TimeProvider, private val servicesOrderPreference: ServicesOrderPreference, @@ -36,8 +36,8 @@ class ExportBackupToDisk( it.mapToRemote() .copy(order = com.twofasapp.prefs.model.RemoteService.Order(position = servicesOrder.ids.indexOf(it.id))) }, - appVersionCode = appConfig.versionCode, - appVersionName = appConfig.versionName, + appVersionCode = appBuild.versionCode, + appVersionName = appBuild.versionName, groups = storeGroups.all().list.filter { group -> group.id != null }.map { group -> group.toRemote() }, account = null, ) @@ -60,6 +60,7 @@ class ExportBackupToDisk( Single.just(ExportBackup.Result.Success(jsonSerializer.serializePretty(result.encryptedRemoteBackup))) } } + is EncryptBackup.Result.Error -> Single.just(ExportBackup.Result.UnknownError) } } diff --git a/backup/src/main/java/com/twofasapp/backup/ui/export/ExportBackupActivity.kt b/backup/src/main/java/com/twofasapp/backup/ui/export/ExportBackupActivity.kt index 87bf1f5d..eed152ca 100644 --- a/backup/src/main/java/com/twofasapp/backup/ui/export/ExportBackupActivity.kt +++ b/backup/src/main/java/com/twofasapp/backup/ui/export/ExportBackupActivity.kt @@ -7,9 +7,9 @@ import android.os.Bundle import androidx.core.content.FileProvider import com.twofasapp.backup.databinding.ActivityExportBackupBinding import com.twofasapp.base.BaseActivityPresenter +import com.twofasapp.common.environment.AppBuild import com.twofasapp.core.RequestCodes import com.twofasapp.design.dialogs.InfoDialog -import com.twofasapp.environment.AppConfig import com.twofasapp.extensions.clicksThrottled import com.twofasapp.extensions.navigationClicksThrottled import com.twofasapp.extensions.toastLong @@ -89,7 +89,7 @@ class ExportBackupActivity : BaseActivityPresenter( outputStream.write(content.toByteArray()) outputStream.close() - val uri = FileProvider.getUriForFile(this, get().id, file) + val uri = FileProvider.getUriForFile(this, get().id, file) val shareIntent = Intent().apply { type = "*/*" diff --git a/base/build.gradle.kts b/base/build.gradle.kts index 35a3522a..940f0903 100644 --- a/base/build.gradle.kts +++ b/base/build.gradle.kts @@ -9,7 +9,7 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":prefs")) implementation(project(":resources")) diff --git a/browserextension/build.gradle.kts b/browserextension/build.gradle.kts index 3ff38222..3edebc60 100644 --- a/browserextension/build.gradle.kts +++ b/browserextension/build.gradle.kts @@ -13,24 +13,25 @@ android { dependencies { implementation(project(":base")) implementation(project(":core")) - implementation(project(":di")) + implementation(project(":core:common")) + implementation(project(":core:di")) implementation(project(":design")) implementation(project(":extensions")) implementation(project(":permissions")) implementation(project(":prefs")) implementation(project(":persistence")) - implementation(project(":network")) implementation(project(":push")) implementation(project(":qrscanner")) - implementation(project(":environment")) + implementation(project(":navigation")) implementation(project(":services:domain")) implementation(project(":time:domain")) implementation(project(":serialization")) implementation(project(":resources")) + implementation(project(":core:designsystem")) + implementation(project(":core:locale")) implementation(project(":security:domain")) - implementation(project(":browserextension:domain")) - + implementation(project(":data:browserext")) implementation(libs.bundles.fastAdapter) implementation(libs.bundles.rxJava) implementation(libs.bundles.appCompat) diff --git a/browserextension/domain/build.gradle.kts b/browserextension/domain/build.gradle.kts deleted file mode 100644 index ac01de3f..00000000 --- a/browserextension/domain/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -@Suppress("DSL_SCOPE_VIOLATION") -plugins { - alias(libs.plugins.twofasAndroidLibrary) - alias(libs.plugins.kotlinParcelize) -} - -android { - namespace = "com.twofasapp.browserextension.domain" -} - - -dependencies { - implementation(project(":base")) - implementation(project(":time:domain")) - implementation(libs.bundles.rxJava) - implementation(libs.kotlinCoroutines) -} diff --git a/browserextension/domain/src/main/AndroidManifest.xml b/browserextension/domain/src/main/AndroidManifest.xml deleted file mode 100644 index d1b2bc81..00000000 --- a/browserextension/domain/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt b/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt deleted file mode 100644 index 39151f36..00000000 --- a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.twofasapp.browserextension.domain - -import com.twofasapp.browserextension.domain.model.TokenRequest - -interface FetchTokenRequestsCase { - suspend operator fun invoke(extensionId: String): List -} \ No newline at end of file diff --git a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt b/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt deleted file mode 100644 index 341c174f..00000000 --- a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.twofasapp.browserextension.domain - -import com.twofasapp.browserextension.domain.model.MobileDevice -import kotlinx.coroutines.flow.Flow - -interface ObserveMobileDeviceCase { - operator fun invoke(): Flow -} \ No newline at end of file diff --git a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt b/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt deleted file mode 100644 index 09e227c3..00000000 --- a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.twofasapp.browserextension.domain - -import com.twofasapp.browserextension.domain.model.PairedBrowser -import kotlinx.coroutines.flow.Flow - -interface ObservePairedBrowsersCase { - operator fun invoke(): Flow> -} \ No newline at end of file diff --git a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/PairedBrowser.kt b/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/PairedBrowser.kt deleted file mode 100644 index 26232d2a..00000000 --- a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/PairedBrowser.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.twofasapp.browserextension.domain.model - -import com.twofasapp.time.domain.formatter.TimeFormatter -import java.time.Instant -import java.time.ZoneOffset - -data class PairedBrowser( - val id: String, - val name: String, - val pairedAt: Instant, - val extensionPublicKey: String, -) { - fun formatPairedAt(): String { - return pairedAt.atOffset(ZoneOffset.UTC).format(TimeFormatter.fullDate) - } -} diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt b/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt index 84f3db93..805a3d53 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/BrowserExtensionModule.kt @@ -1,20 +1,20 @@ package com.twofasapp.browserextension -import com.twofasapp.browserextension.data.BrowserExtensionLocalData -import com.twofasapp.browserextension.data.BrowserExtensionLocalDataImpl -import com.twofasapp.browserextension.data.BrowserExtensionRemoteData -import com.twofasapp.browserextension.data.BrowserExtensionRemoteDataImpl -import com.twofasapp.browserextension.domain.* -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepositoryImpl +import com.twofasapp.browserextension.domain.ApproveLoginRequestCase +import com.twofasapp.browserextension.domain.DeletePairedBrowserCase +import com.twofasapp.browserextension.domain.DenyLoginRequestCase +import com.twofasapp.browserextension.domain.EncryptCodeCase +import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase +import com.twofasapp.browserextension.domain.FetchTokenRequestsCase +import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase +import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase +import com.twofasapp.browserextension.domain.PairBrowserCase +import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase +import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase import com.twofasapp.browserextension.notification.ShowBrowserExtensionRequestNotificationCaseImpl -import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreenFactory import com.twofasapp.browserextension.ui.browser.BrowserDetailsViewModel -import com.twofasapp.browserextension.ui.main.BrowserExtensionScreenFactory import com.twofasapp.browserextension.ui.main.BrowserExtensionViewModel -import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreenFactory import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressViewModel -import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreenFactory import com.twofasapp.browserextension.ui.pairing.scan.PairingScanViewModel import com.twofasapp.browserextension.ui.request.BrowserExtensionRequestViewModel import com.twofasapp.di.KoinModule @@ -27,19 +27,15 @@ import org.koin.dsl.module class BrowserExtensionModule : KoinModule { override fun provide() = module { - singleOf(::BrowserExtensionLocalDataImpl) { bind() } - singleOf(::BrowserExtensionRemoteDataImpl) { bind() } - singleOf(::BrowserExtensionRepositoryImpl) { bind() } - singleOf(::ShowBrowserExtensionRequestNotificationCaseImpl) { bind() } singleOf(::RegisterMobileDeviceCase) singleOf(::PairBrowserCase) - singleOf(::ObserveMobileDeviceCaseImpl) { bind() } + singleOf(::ObserveMobileDeviceCase) singleOf(::UpdateMobileDeviceCase) - singleOf(::ObservePairedBrowsersCaseImpl) { bind() } + singleOf(::ObservePairedBrowsersCase) singleOf(::FetchPairedBrowsersCase) - singleOf(::FetchTokenRequestsCaseImpl) { bind() } + singleOf(::FetchTokenRequestsCase) singleOf(::ApproveLoginRequestCase) singleOf(::DenyLoginRequestCase) singleOf(::EncryptCodeCase) @@ -50,10 +46,5 @@ class BrowserExtensionModule : KoinModule { viewModelOf(::PairingProgressViewModel) viewModelOf(::BrowserExtensionRequestViewModel) viewModelOf(::BrowserDetailsViewModel) - - singleOf(::BrowserExtensionScreenFactory) - singleOf(::PairingProgressScreenFactory) - singleOf(::PairingScanScreenFactory) - singleOf(::BrowserDetailsScreenFactory) } } \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalData.kt b/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalData.kt deleted file mode 100644 index 3fa8cc1d..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalData.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.twofasapp.browserextension.data - -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.model.PairedBrowser -import kotlinx.coroutines.flow.Flow - -internal interface BrowserExtensionLocalData { - fun observeMobileDevice(): Flow - fun observePairedBrowsers(): Flow> - suspend fun saveMobileDevice(mobileDevice: MobileDevice) - suspend fun savePairedBrowser(pairedBrowser: PairedBrowser) - suspend fun updatePairedBrowsers(pairedBrowsers: List) -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalDataImpl.kt b/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalDataImpl.kt deleted file mode 100644 index 52978f46..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionLocalDataImpl.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.twofasapp.browserextension.data - -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.model.PairedBrowser -import com.twofasapp.persistence.dao.PairedBrowserDao -import com.twofasapp.persistence.model.PairedBrowserEntity -import com.twofasapp.prefs.model.MobileDeviceEntity -import com.twofasapp.prefs.usecase.MobileDevicePreference -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import java.time.Instant - -internal class BrowserExtensionLocalDataImpl( - private val mobileDevicePreference: MobileDevicePreference, - private val dao: PairedBrowserDao, -) : BrowserExtensionLocalData { - - override fun observeMobileDevice(): Flow { - return mobileDevicePreference.flow().map { it.toDomain() } - } - - override fun observePairedBrowsers(): Flow> { - return dao.observe() - .map { list -> list.map { it.toDomain() } } - } - - override suspend fun saveMobileDevice(mobileDevice: MobileDevice) { - mobileDevicePreference.put(mobileDevice.toEntity()) - } - - override suspend fun savePairedBrowser(pairedBrowser: PairedBrowser) { - dao.insertOrUpdate(pairedBrowser.toEntity()) - } - - override suspend fun updatePairedBrowsers(pairedBrowsers: List) { - dao.updateAll(pairedBrowsers.map { it.toEntity() }) - } - - private fun MobileDevice.toEntity() = - MobileDeviceEntity( - id = id, - name = name, - fcmToken = fcmToken, - platform = platform, - publicKey = publicKey, - ) - - private fun MobileDeviceEntity.toDomain() = - MobileDevice( - id = id, - name = name, - fcmToken = fcmToken, - platform = platform, - publicKey = publicKey, - ) - - private fun PairedBrowser.toEntity() = - PairedBrowserEntity( - id = id, - name = name, - extensionPublicKey = extensionPublicKey, - pairedAt = pairedAt.toEpochMilli(), - ) - - private fun PairedBrowserEntity.toDomain() = - PairedBrowser( - id = id, - name = name, - pairedAt = Instant.ofEpochMilli(pairedAt), - extensionPublicKey = extensionPublicKey, - ) -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteData.kt b/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteData.kt deleted file mode 100644 index 1ece62d4..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteData.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.twofasapp.browserextension.data - -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.model.PairedBrowser -import com.twofasapp.browserextension.domain.model.TokenRequest - -internal interface BrowserExtensionRemoteData { - suspend fun registerMobileDevice( - deviceName: String, - devicePublicKey: String, - fcmToken: String, - platform: String, - ): MobileDevice - - suspend fun updateMobileDevice( - deviceId: String, - newName: String - ) - - suspend fun pairBrowser( - deviceId: String, - extensionId: String, - deviceName: String, - devicePublicKey: String, - ): PairedBrowser - - suspend fun updatePairedBrowser( - extensionId: String, - newName: String - ) - - suspend fun deletePairedBrowser( - deviceId: String, - extensionId: String - ) - - suspend fun getBrowsers( - deviceId: String, - ): List - - suspend fun acceptLoginRequest( - deviceId: String, - extensionId: String, - requestId: String, - code: String - ) - - suspend fun denyLoginRequest( - extensionId: String, - requestId: String, - ) - - suspend fun fetchTokenRequests( - extensionId: String, - ): List -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteDataImpl.kt b/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteDataImpl.kt deleted file mode 100644 index c1234a03..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/data/BrowserExtensionRemoteDataImpl.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.twofasapp.browserextension.data - -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.model.PairedBrowser -import com.twofasapp.browserextension.domain.model.TokenRequest -import com.twofasapp.network.api.BrowserExtensionApi -import com.twofasapp.network.body.ApproveLoginRequestBody -import com.twofasapp.network.body.DeviceRegisterBody -import com.twofasapp.network.body.PairBrowserBody -import java.time.Instant - -internal class BrowserExtensionRemoteDataImpl( - private val api: BrowserExtensionApi, -) : BrowserExtensionRemoteData { - - override suspend fun registerMobileDevice(deviceName: String, devicePublicKey: String, fcmToken: String, platform: String): MobileDevice { - val response = api.registerMobileDevice( - DeviceRegisterBody( - name = deviceName, - fcm_token = fcmToken, - platform = platform, - ) - ) - - return MobileDevice( - id = response.id, - name = response.name, - fcmToken = fcmToken, - platform = response.platform, - publicKey = devicePublicKey, - ) - } - - override suspend fun updateMobileDevice(deviceId: String, newName: String) { - api.updateMobileDevice(deviceId, newName) - } - - override suspend fun pairBrowser(deviceId: String, extensionId: String, deviceName: String, devicePublicKey: String): PairedBrowser { - val pairResponse = api.pairBrowser( - deviceId = deviceId, - body = PairBrowserBody( - extension_id = extensionId, - device_name = deviceName, - device_public_key = devicePublicKey, - ) - ) - - val browserResponse = api.getBrowser(deviceId = deviceId, extensionId = extensionId) - - return PairedBrowser( - id = browserResponse.id, - name = browserResponse.name, - pairedAt = Instant.parse(browserResponse.paired_at), - extensionPublicKey = pairResponse.extension_public_key - ) - } - - override suspend fun updatePairedBrowser(extensionId: String, newName: String) { - api.updateBrowserName(extensionId, newName) - } - - override suspend fun deletePairedBrowser(deviceId: String, extensionId: String) { - api.deletePairedBrowser(deviceId, extensionId) - } - - override suspend fun getBrowsers(deviceId: String): List { - return api.getBrowsers(deviceId).map { - PairedBrowser( - id = it.id, - name = it.name, - pairedAt = Instant.parse(it.paired_at), - extensionPublicKey = "", - ) - } - } - - override suspend fun acceptLoginRequest(deviceId: String, extensionId: String, requestId: String, code: String) { - api.acceptLoginRequest( - deviceId = deviceId, - body = ApproveLoginRequestBody( - extension_id = extensionId, - token_request_id = requestId, - token = code, - ) - ) - } - - override suspend fun denyLoginRequest(extensionId: String, requestId: String) { - api.denyLoginRequest( - extensionId = extensionId, - tokenRequestId = requestId, - ) - } - - override suspend fun fetchTokenRequests(deviceId: String): List { - return api.fetchTokenRequests(deviceId) - .filter { it.status.equals("pending", true) } - .map { - TokenRequest( - domain = it.domain, - requestId = it.token_request_id, - extensionId = it.extension_id, - ) - } - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ApproveLoginRequestCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ApproveLoginRequestCase.kt index b360af39..d7777f27 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ApproveLoginRequestCase.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ApproveLoginRequestCase.kt @@ -1,10 +1,10 @@ package com.twofasapp.browserextension.domain -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository +import com.twofasapp.data.browserext.BrowserExtRepository import kotlinx.coroutines.flow.first -internal class ApproveLoginRequestCase( - private val browserExtensionRepository: BrowserExtensionRepository, +class ApproveLoginRequestCase( + private val browserExtensionRepository: BrowserExtRepository, private val observeMobileDeviceCase: ObserveMobileDeviceCase, private val observePairedBrowsersCase: ObservePairedBrowsersCase, private val encryptCodeCase: EncryptCodeCase, diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/DeletePairedBrowserCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/DeletePairedBrowserCase.kt index f8ad643a..46c74520 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/DeletePairedBrowserCase.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/DeletePairedBrowserCase.kt @@ -1,11 +1,11 @@ package com.twofasapp.browserextension.domain -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository +import com.twofasapp.data.browserext.BrowserExtRepository import kotlinx.coroutines.flow.first -internal class DeletePairedBrowserCase( +class DeletePairedBrowserCase( private val observeMobileDeviceCase: ObserveMobileDeviceCase, - private val browserExtensionRepository: BrowserExtensionRepository, + private val browserExtensionRepository: BrowserExtRepository, ) { suspend operator fun invoke(extensionId: String) { return browserExtensionRepository.deletePairedBrowser( diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/DenyLoginRequestCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/DenyLoginRequestCase.kt index fb8ee0a7..cb0e8d21 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/DenyLoginRequestCase.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/DenyLoginRequestCase.kt @@ -1,9 +1,9 @@ package com.twofasapp.browserextension.domain -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository +import com.twofasapp.data.browserext.BrowserExtRepository -internal class DenyLoginRequestCase( - private val browserExtensionRepository: BrowserExtensionRepository, +class DenyLoginRequestCase( + private val browserExtensionRepository: BrowserExtRepository, ) { suspend operator fun invoke( extensionId: String, diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchPairedBrowsersCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchPairedBrowsersCase.kt index d916c349..b4ebe1c0 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchPairedBrowsersCase.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchPairedBrowsersCase.kt @@ -1,9 +1,9 @@ package com.twofasapp.browserextension.domain -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository +import com.twofasapp.data.browserext.BrowserExtRepository -internal class FetchPairedBrowsersCase( - private val browserExtensionRepository: BrowserExtensionRepository +class FetchPairedBrowsersCase( + private val browserExtensionRepository: BrowserExtRepository ) { suspend operator fun invoke() { diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt new file mode 100644 index 00000000..22c4f7ed --- /dev/null +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCase.kt @@ -0,0 +1,15 @@ +package com.twofasapp.browserextension.domain + +import com.twofasapp.data.browserext.BrowserExtRepository +import com.twofasapp.data.browserext.domain.TokenRequest + +class FetchTokenRequestsCase( + private val browserExtensionRepository: BrowserExtRepository +) { + + suspend operator fun invoke(deviceId: String): List { + if (deviceId.isBlank()) return emptyList() + + return browserExtensionRepository.fetchTokenRequests(deviceId) + } +} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCaseImpl.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCaseImpl.kt deleted file mode 100644 index 84711df9..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/FetchTokenRequestsCaseImpl.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.twofasapp.browserextension.domain - -import com.twofasapp.browserextension.domain.model.TokenRequest -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository - -internal class FetchTokenRequestsCaseImpl( - private val browserExtensionRepository: BrowserExtensionRepository -) : FetchTokenRequestsCase { - - override suspend operator fun invoke(deviceId: String): List { - if(deviceId.isBlank()) return emptyList() - - return browserExtensionRepository.fetchTokenRequests(deviceId) - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt new file mode 100644 index 00000000..1d1ffd48 --- /dev/null +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCase.kt @@ -0,0 +1,14 @@ +package com.twofasapp.browserextension.domain + +import com.twofasapp.data.browserext.BrowserExtRepository +import com.twofasapp.data.browserext.domain.MobileDevice +import kotlinx.coroutines.flow.Flow + +class ObserveMobileDeviceCase( + private val browserExtensionRepository: BrowserExtRepository +) { + + operator fun invoke(): Flow { + return browserExtensionRepository.observeMobileDevice() + } +} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCaseImpl.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCaseImpl.kt deleted file mode 100644 index 42b0fdd0..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObserveMobileDeviceCaseImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.twofasapp.browserextension.domain - -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository -import kotlinx.coroutines.flow.Flow - -internal class ObserveMobileDeviceCaseImpl( - private val browserExtensionRepository: BrowserExtensionRepository -) : ObserveMobileDeviceCase { - - override operator fun invoke(): Flow { - return browserExtensionRepository.observeMobileDevice() - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt new file mode 100644 index 00000000..d0d0145a --- /dev/null +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCase.kt @@ -0,0 +1,14 @@ +package com.twofasapp.browserextension.domain + +import com.twofasapp.data.browserext.BrowserExtRepository +import com.twofasapp.data.browserext.domain.PairedBrowser +import kotlinx.coroutines.flow.Flow + +class ObservePairedBrowsersCase( + private val browserExtensionRepository: BrowserExtRepository +) { + + operator fun invoke(): Flow> { + return browserExtensionRepository.observePairedBrowsers() + } +} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCaseImpl.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCaseImpl.kt deleted file mode 100644 index 5bfffa02..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/ObservePairedBrowsersCaseImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.twofasapp.browserextension.domain - -import com.twofasapp.browserextension.domain.model.PairedBrowser -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository -import kotlinx.coroutines.flow.Flow - -internal class ObservePairedBrowsersCaseImpl( - private val browserExtensionRepository: BrowserExtensionRepository -) : ObservePairedBrowsersCase { - - override operator fun invoke(): Flow> { - return browserExtensionRepository.observePairedBrowsers() - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/PairBrowserCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/PairBrowserCase.kt index 0be78f52..fd7ea79c 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/PairBrowserCase.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/PairBrowserCase.kt @@ -1,10 +1,10 @@ package com.twofasapp.browserextension.domain -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository +import com.twofasapp.data.browserext.BrowserExtRepository import kotlinx.coroutines.flow.first -internal class PairBrowserCase( - private val browserExtensionRepository: BrowserExtensionRepository, +class PairBrowserCase( + private val browserExtensionRepository: BrowserExtRepository, private val observeMobileDeviceCase: ObserveMobileDeviceCase, ) { diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/RegisterMobileDeviceCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/RegisterMobileDeviceCase.kt index c2643c96..20fb248a 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/RegisterMobileDeviceCase.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/RegisterMobileDeviceCase.kt @@ -2,21 +2,21 @@ package com.twofasapp.browserextension.domain import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository +import com.twofasapp.common.environment.AppBuild import com.twofasapp.core.encoding.encodeBase64ToString -import com.twofasapp.environment.AppConfig +import com.twofasapp.data.browserext.BrowserExtRepository +import com.twofasapp.data.browserext.domain.MobileDevice import com.twofasapp.push.domain.GetFcmTokenCase import kotlinx.coroutines.flow.first import java.security.KeyPair import java.security.KeyPairGenerator import java.security.KeyStore -internal class RegisterMobileDeviceCase( - private val browserExtensionRepository: BrowserExtensionRepository, +class RegisterMobileDeviceCase( + private val browserExtensionRepository: BrowserExtRepository, private val observeMobileDeviceCase: ObserveMobileDeviceCase, private val getFcmTokenCase: GetFcmTokenCase, - private val appConfig: AppConfig, + private val appBuild: AppBuild, ) { companion object { @@ -31,7 +31,7 @@ internal class RegisterMobileDeviceCase( mobileDevice } else { browserExtensionRepository.registerMobileDevice( - deviceName = mobileDevice.name.ifBlank { appConfig.deviceName }, + deviceName = mobileDevice.name.ifBlank { appBuild.deviceName }, devicePublicKey = createDevicePublicKey(), fcmToken = fcmToken, ) diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/UpdateMobileDeviceCase.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/UpdateMobileDeviceCase.kt index f5d790eb..0ef28865 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/UpdateMobileDeviceCase.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/domain/UpdateMobileDeviceCase.kt @@ -1,10 +1,10 @@ package com.twofasapp.browserextension.domain -import com.twofasapp.browserextension.domain.repository.BrowserExtensionRepository +import com.twofasapp.data.browserext.BrowserExtRepository import kotlinx.coroutines.flow.first -internal class UpdateMobileDeviceCase( - private val browserExtensionRepository: BrowserExtensionRepository +class UpdateMobileDeviceCase( + private val browserExtensionRepository: BrowserExtRepository ) { data class Params( diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepositoryImpl.kt b/browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepositoryImpl.kt deleted file mode 100644 index fd7d2afd..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepositoryImpl.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.twofasapp.browserextension.domain.repository - -import com.twofasapp.browserextension.data.BrowserExtensionLocalData -import com.twofasapp.browserextension.data.BrowserExtensionRemoteData -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.model.PairedBrowser -import com.twofasapp.browserextension.domain.model.TokenRequest -import com.twofasapp.extensions.ifNotBlank -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first - -internal class BrowserExtensionRepositoryImpl( - private val localData: BrowserExtensionLocalData, - private val remoteData: BrowserExtensionRemoteData, -) : BrowserExtensionRepository { - - companion object { - private const val PLATFORM = "android" - } - - override fun observeMobileDevice(): Flow { - return localData.observeMobileDevice() - } - - override fun observePairedBrowsers(): Flow> { - return localData.observePairedBrowsers() - } - - override suspend fun updateMobileDevice(mobileDevice: MobileDevice) { - remoteData.updateMobileDevice(mobileDevice.id, mobileDevice.name) - localData.saveMobileDevice(mobileDevice) - } - - override suspend fun registerMobileDevice(deviceName: String, devicePublicKey: String, fcmToken: String): MobileDevice { - val mobileDevice = remoteData.registerMobileDevice( - deviceName = deviceName, - devicePublicKey = devicePublicKey, - fcmToken = fcmToken, - platform = PLATFORM, - ) - - localData.saveMobileDevice(mobileDevice) - return mobileDevice - } - - override suspend fun pairBrowser(deviceId: String, extensionId: String, deviceName: String, devicePublicKey: String): PairedBrowser { - val browser = remoteData.pairBrowser( - deviceId = deviceId, - extensionId = extensionId, - deviceName = deviceName, - devicePublicKey = devicePublicKey - ) - localData.savePairedBrowser(browser) - return browser - } - - override suspend fun updatePairedBrowser(extensionId: String, newName: String) { - remoteData.updatePairedBrowser(extensionId = extensionId, newName = newName) - fetchPairedBrowsers() - } - - override suspend fun fetchPairedBrowsers() { - localData.observeMobileDevice().first().id.ifNotBlank { id -> - localData.updatePairedBrowsers(remoteData.getBrowsers(id)) - } - } - - override suspend fun fetchTokenRequests(deviceId: String): List { - return remoteData.fetchTokenRequests(deviceId) - } - - override suspend fun deletePairedBrowser(deviceId: String, extensionId: String) { - remoteData.deletePairedBrowser(deviceId, extensionId) - fetchPairedBrowsers() - } - - override suspend fun acceptLoginRequest(deviceId: String, extensionId: String, requestId: String, code: String) { - return remoteData.acceptLoginRequest(deviceId, extensionId, requestId, code) - } - - override suspend fun denyLoginRequest(extensionId: String, requestId: String) { - return remoteData.denyLoginRequest(extensionId, requestId) - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreen.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreen.kt index 901e8f43..e29a51cb 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreen.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreen.kt @@ -4,42 +4,39 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.* +import androidx.compose.material3.Divider +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.twofasapp.resources.R import com.twofasapp.design.compose.SimpleEntry -import com.twofasapp.design.compose.Toolbar import com.twofasapp.design.compose.dialogs.ConfirmDialog -import com.twofasapp.design.compose.dialogs.InputDialog -import com.twofasapp.design.theme.divider -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.resources.R import kotlinx.coroutines.launch -import org.koin.androidx.compose.get +import org.koin.androidx.compose.koinViewModel @Composable -internal fun BrowserDetailsScreen( +fun BrowserDetailsScreen( + onFinish: () -> Unit, extensionId: String, - viewModel: BrowserDetailsViewModel = get(), - router: SettingsRouter = get(), + viewModel: BrowserDetailsViewModel = koinViewModel(), ) { viewModel.init(extensionId) val uiState = viewModel.uiState.collectAsState().value - val scaffoldState = rememberScaffoldState() val scope = rememberCoroutineScope() + viewModel.onFinish = onFinish Scaffold( - scaffoldState = scaffoldState, +// scaffoldState = scaffoldState, topBar = { - Toolbar(title = stringResource(id = R.string.browser__browser_extension)) { - router.navigate(SettingsDirections.GoBack) - } + TwTopAppBar(titleText = stringResource(id = R.string.browser__browser_extension)) } ) { padding -> LazyColumn(modifier = Modifier.padding(padding)) { @@ -56,13 +53,13 @@ internal fun BrowserDetailsScreen( subtitle = uiState.browserPairedAt, ) } - item { Divider(color = MaterialTheme.colors.divider, modifier = Modifier.padding(vertical = 8.dp)) } + item { Divider(color = TwTheme.color.divider, modifier = Modifier.padding(vertical = 8.dp)) } item { OutlinedButton( onClick = { viewModel.showConfirmForget() }, shape = CircleShape, modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.primary) + border = BorderStroke(1.dp, TwTheme.color.primary) ) { Text(text = "Forget this web browser") } @@ -86,8 +83,8 @@ internal fun BrowserDetailsScreen( when (it) { is BrowserDetailsUiState.Event.ShowSnackbarError -> { scope.launch { - scaffoldState.snackbarHostState.currentSnackbarData?.dismiss() - scaffoldState.snackbarHostState.showSnackbar(it.message) +// scaffoldState.snackbarHostState.currentSnackbarData?.dismiss() +// scaffoldState.snackbarHostState.showSnackbar(it.message) } } } diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreenFactory.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreenFactory.kt deleted file mode 100644 index 2c8dc752..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsScreenFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.twofasapp.browserextension.ui.browser - -import androidx.compose.runtime.Composable -import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreen - -class BrowserDetailsScreenFactory { - - @Composable - fun create(extensionId: String) { - BrowserDetailsScreen(extensionId) - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsUiState.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsUiState.kt index 3be47dbd..bc86452c 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsUiState.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsUiState.kt @@ -3,7 +3,7 @@ package com.twofasapp.browserextension.ui.browser import com.twofasapp.base.UiEvent import com.twofasapp.base.UiState -internal data class BrowserDetailsUiState( +data class BrowserDetailsUiState( val extensionId: String = "", val browserName: String = "", val browserPairedAt: String = "", diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsViewModel.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsViewModel.kt index de648457..a2c507f1 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsViewModel.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/browser/BrowserDetailsViewModel.kt @@ -5,26 +5,26 @@ import com.twofasapp.base.BaseViewModel import com.twofasapp.base.dispatcher.Dispatchers import com.twofasapp.browserextension.domain.DeletePairedBrowserCase import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -internal class BrowserDetailsViewModel( +class BrowserDetailsViewModel( private val dispatchers: Dispatchers, private val observePairedBrowsersCase: ObservePairedBrowsersCase, private val deletePairedBrowserCase: DeletePairedBrowserCase, - private val settingsRouter: SettingsRouter, ) : BaseViewModel() { + var onFinish: () -> Unit = {} + private val _uiState = MutableStateFlow(BrowserDetailsUiState()) val uiState = _uiState.asStateFlow() fun init(extensionId: String) { - viewModelScope.launch { + viewModelScope.launch(dispatchers.io()) { observePairedBrowsersCase().flowOn(dispatchers.io()).collect { list -> val browser = list.find { it.id == extensionId } @@ -33,7 +33,7 @@ internal class BrowserDetailsViewModel( it.copy( extensionId = extensionId, browserName = browser.name, - browserPairedAt = browser.formatPairedAt(), +// browserPairedAt = browser.formatPairedAt(), ) } } @@ -54,7 +54,7 @@ internal class BrowserDetailsViewModel( viewModelScope.launch(dispatchers.io()) { runSafely(catch = { postError() }) { deletePairedBrowserCase(uiState.value.extensionId) - settingsRouter.navigate(SettingsDirections.GoBack) + onFinish() // TODO: FIX } } } diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt index 1440b824..bd6ad4d7 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreen.kt @@ -1,20 +1,21 @@ package com.twofasapp.browserextension.ui.main -import android.app.Activity -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -22,39 +23,51 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import com.twofasapp.resources.R -import com.twofasapp.design.compose.* +import com.twofasapp.design.compose.SimpleEntry import com.twofasapp.design.compose.dialogs.InputDialog import com.twofasapp.design.compose.dialogs.RationaleDialog -import com.twofasapp.extensions.openBrowserApp -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwButton +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.designsystem.screen.CommonContent +import com.twofasapp.designsystem.settings.SettingsHeader +import com.twofasapp.designsystem.settings.SettingsLink +import com.twofasapp.locale.TwLocale +import com.twofasapp.resources.R import kotlinx.coroutines.launch -import org.koin.androidx.compose.get +import org.koin.androidx.compose.koinViewModel @Composable -internal fun BrowserExtensionScreen( - viewModel: BrowserExtensionViewModel = get(), - router: SettingsRouter = get(), +fun BrowserExtensionScreen( + openPairingScan: () -> Unit, + openBrowserDetails: (String) -> Unit, + viewModel: BrowserExtensionViewModel = koinViewModel(), ) { val uiState = viewModel.uiState.collectAsState().value - val scaffoldState = rememberScaffoldState() + val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + viewModel.onPairClick = { openPairingScan() } Scaffold( - scaffoldState = scaffoldState, - topBar = { Toolbar(title = stringResource(id = R.string.browser__browser_extension)) { router.navigate(SettingsDirections.GoBack) } }, + topBar = { TwTopAppBar(titleText = TwLocale.strings.browserExtTitle) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, ) { padding -> if (uiState.isLoading) return@Scaffold if (uiState.pairedBrowsers.isEmpty()) { - EmptyScreen(viewModel, padding) + EmptyScreen( + onPairBrowserClick = openPairingScan, + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp) + ) } else { ContentScreen( + onBrowserClick = openBrowserDetails, + onPairBrowserClick = openPairingScan, viewModel = viewModel, uiState = uiState, - router = router, padding = padding ) } @@ -64,8 +77,8 @@ internal fun BrowserExtensionScreen( when (it) { is BrowserExtensionUiState.Event.ShowSnackbarError -> { scope.launch { - scaffoldState.snackbarHostState.currentSnackbarData?.dismiss() - scaffoldState.snackbarHostState.showSnackbar(it.message) + snackbarHostState.currentSnackbarData?.dismiss() + snackbarHostState.showSnackbar(it.message) viewModel.eventHandled(it.id) } } @@ -74,44 +87,37 @@ internal fun BrowserExtensionScreen( } @Composable -internal fun ContentScreen( +private fun ContentScreen( + onBrowserClick: (String) -> Unit, + onPairBrowserClick: () -> Unit, viewModel: BrowserExtensionViewModel, uiState: BrowserExtensionUiState, - router: SettingsRouter, padding: PaddingValues, ) { LazyColumn(modifier = Modifier.padding(padding)) { - item { - HeaderEntry(text = stringResource(id = R.string.browser__paired_devices_browser_title)) - } + item { SettingsHeader(TwLocale.strings.browserExtPairedDevices) } items(uiState.pairedBrowsers, key = { it.id }) { - SimpleEntry( - title = it.name, - iconVisibleWhenNotSet = true, - subtitle = it.formatPairedAt(), - click = { router.navigate(SettingsDirections.BrowserDetails(extensionId = it.id)) } + SettingsLink(it.name, onClick = { onBrowserClick(it.id) }) + // subtitle = it.formatPairedAt(), + } + + item { + TwButton( + text = "+ ${TwLocale.strings.browserExtAddNew}", + onClick = onPairBrowserClick, + height = TwTheme.dimen.buttonHeightSmall, + modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp) ) } - item { - Button( - onClick = { viewModel.onPairBrowserClick() }, - shape = ButtonShape(), - modifier = Modifier.padding(start = 72.dp, top = 6.dp, bottom = 2.dp) - ) { - Text(text = "+ Add new".uppercase(), color = ButtonTextColor()) - } - } - - item { - HeaderEntry(text = stringResource(id = R.string.browser__this_device_name)) - } + item { SettingsHeader(TwLocale.strings.browserExtDeviceName) } + // TODO item { SimpleEntry( title = uiState.mobileDevice?.name.orEmpty(), - subtitle = stringResource(id = R.string.browser__this_device_footer), + subtitle = TwLocale.strings.browserExtDeviceNameSubtitle, iconEnd = painterResource(id = R.drawable.ic_toolbar_edit), iconEndClick = { viewModel.onEditDeviceClick() }, ) @@ -139,74 +145,34 @@ internal fun ContentScreen( } @Composable -internal fun EmptyScreen( - viewModel: BrowserExtensionViewModel, - padding: PaddingValues, +private fun EmptyScreen( + onPairBrowserClick: () -> Unit, + modifier: Modifier, ) { - val activity = (LocalContext.current as? Activity) + val uriHandler = LocalUriHandler.current - ConstraintLayout(modifier = Modifier - .fillMaxSize() - .padding(padding)) { - val (content, pair) = createRefs() - - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .constrainAs(content) { - top.linkTo(parent.top) - bottom.linkTo(pair.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - } - .padding(vertical = 16.dp) - ) { - Image( - painter = painterResource(id = R.drawable.browser_extension_start_image), - contentDescription = null, + CommonContent( + image = painterResource(id = R.drawable.browser_extension_start_image), + titleText = TwLocale.strings.browserExtHeader, + descriptionText = "${TwLocale.strings.browserExtBody1}\n${TwLocale.strings.browserExtBody2}", + ctaPrimaryText = TwLocale.strings.browserExtCta, + ctaPrimaryClick = onPairBrowserClick, + description = { + Text( + text = buildAnnotatedString { + append("${TwLocale.strings.browserExtMore1} ") + withStyle(style = SpanStyle(TwTheme.color.primary)) { + append(TwLocale.strings.browserExtMore2) + } + }, + style = TwTheme.typo.body2, + textAlign = TextAlign.Center, modifier = Modifier - .height(130.dp) - .offset(y = (-16).dp) + .padding(horizontal = 16.dp) + .padding(top = 16.dp) + .clickable { uriHandler.openUri(TwLocale.links.browserExt) }, ) - - Text( - text = "2FAS Web Browser extension", - style = MaterialTheme.typography.h6, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 16.dp) - ) - - Text( - text = "1. Install the 2FAS browser extension on your desktop computer.\n2. Pair it with your 2FAS app.", - style = MaterialTheme.typography.body1, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 16.dp) - ) - - Text(text = buildAnnotatedString { - append("More info: ") - withStyle(style = SpanStyle(MaterialTheme.colors.primary)) { - append("2fas.com/be") - } - }, style = MaterialTheme.typography.body2, modifier = Modifier - .padding(horizontal = 16.dp) - .align(CenterHorizontally) - .clickable { - activity?.openBrowserApp(url = "https://2fas.com/be") - }, textAlign = TextAlign.Center) - } - - Button(onClick = { viewModel.onPairBrowserClick() }, - shape = ButtonShape(), - modifier = Modifier - .height(48.dp) - .constrainAs(pair) { - bottom.linkTo(parent.bottom, margin = 16.dp) - start.linkTo(parent.start) - end.linkTo(parent.end) - }) { - Text(text = "Pair with web browser".uppercase(), color = ButtonTextColor()) - } - } + }, + modifier = modifier, + ) } \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreenFactory.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreenFactory.kt deleted file mode 100644 index 0afd2b3e..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.browserextension.ui.main - -import androidx.compose.runtime.Composable - -class BrowserExtensionScreenFactory { - - @Composable - fun create() { - BrowserExtensionScreen() - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt index 2245b400..c20f019d 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionUiState.kt @@ -2,10 +2,10 @@ package com.twofasapp.browserextension.ui.main import com.twofasapp.base.UiEvent import com.twofasapp.base.UiState -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.model.PairedBrowser +import com.twofasapp.data.browserext.domain.MobileDevice +import com.twofasapp.data.browserext.domain.PairedBrowser -internal data class BrowserExtensionUiState( +data class BrowserExtensionUiState( val isLoading: Boolean = true, val pairedBrowsers: List = emptyList(), val mobileDevice: MobileDevice? = null, diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt index c83e41be..3b37f46f 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/main/BrowserExtensionViewModel.kt @@ -7,16 +7,20 @@ import com.twofasapp.browserextension.domain.FetchPairedBrowsersCase import com.twofasapp.browserextension.domain.ObserveMobileDeviceCase import com.twofasapp.browserextension.domain.ObservePairedBrowsersCase import com.twofasapp.browserextension.domain.UpdateMobileDeviceCase -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter import com.twofasapp.permissions.CameraPermissionRequestFlow import com.twofasapp.permissions.PermissionStatus -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -internal class BrowserExtensionViewModel( +class BrowserExtensionViewModel( private val dispatchers: Dispatchers, - private val settingsRouter: SettingsRouter, private val cameraPermissionRequest: CameraPermissionRequestFlow, private val observeMobileDeviceCase: ObserveMobileDeviceCase, private val observePairedBrowsersCase: ObservePairedBrowsersCase, @@ -27,6 +31,8 @@ internal class BrowserExtensionViewModel( private val _uiState = MutableStateFlow(BrowserExtensionUiState()) val uiState = _uiState.asStateFlow() + var onPairClick: () -> Unit = {} + init { viewModelScope.launch { @@ -56,7 +62,10 @@ internal class BrowserExtensionViewModel( .take(1) .onEach { when (it) { - PermissionStatus.GRANTED -> settingsRouter.navigate(SettingsDirections.PairingScan) + PermissionStatus.GRANTED -> { + onPairClick() + } + PermissionStatus.DENIED -> Unit PermissionStatus.DENIED_NEVER_ASK -> _uiState.update { state -> state.copy(showRationaleDialog = true) diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt index c14d9dc2..0500921b 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreen.kt @@ -1,11 +1,14 @@ package com.twofasapp.browserextension.ui.pairing.progress import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -16,33 +19,46 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout -import com.airbnb.lottie.compose.* +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.twofasapp.design.compose.AnimatedContent +import com.twofasapp.design.compose.ButtonHeight +import com.twofasapp.design.compose.ButtonShape +import com.twofasapp.design.compose.ButtonTextColor +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.resources.R -import com.twofasapp.design.compose.* -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter -import org.koin.androidx.compose.get +import org.koin.androidx.compose.koinViewModel @Composable -internal fun PairingProgressScreen( +fun PairingProgressScreen( + openMain: () -> Unit, + openPairingScan: () -> Unit, extensionId: String, - viewModel: PairingProgressViewModel = get(), - router: SettingsRouter = get(), + viewModel: PairingProgressViewModel = koinViewModel(), ) { val uiState = viewModel.uiState.collectAsState() viewModel.pairBrowser(extensionId) Scaffold( topBar = { - Toolbar(title = stringResource(id = if (uiState.value.isPairing) R.string.browser__pairing_with_browser else R.string.settings__browser_extension_result_toolbar_title)) { - router.navigate(SettingsDirections.GoBack) - } + TwTopAppBar(titleText = stringResource(id = if (uiState.value.isPairing) R.string.browser__pairing_with_browser else R.string.settings__browser_extension_result_toolbar_title)) } ) { padding -> AnimatedContent( condition = uiState.value.isPairing, contentWhenTrue = { ProgressContent() }, - contentWhenFalse = { ResultContent(uiState.value.isPairingSuccess, uiState.value.code, router) } + contentWhenFalse = { + ResultContent( + onContinueClick = { openMain() }, + onScanAgainClick = { openPairingScan() }, + uiState.value.isPairingSuccess, + uiState.value.code, + padding + ) + }, ) } } @@ -57,9 +73,11 @@ internal fun ProgressContent() { @Composable internal fun ResultContent( + onContinueClick: () -> Unit = {}, + onScanAgainClick: () -> Unit = {}, isSuccess: Boolean, code: Int? = null, - router: SettingsRouter, + padding: PaddingValues, ) { val image = if (isSuccess) R.drawable.browser_extension_success_image else R.drawable.browser_extension_error_image @@ -77,15 +95,19 @@ internal fun ResultContent( val cta = if (isSuccess) R.string.commons__continue else R.string.browser__result_error_cta - val ctaAction: () -> Unit = if (isSuccess) { - { router.navigate(SettingsDirections.GoBack) } - } else { - { router.navigate(SettingsDirections.PairingScan) } + val ctaAction: () -> Unit = { + if (isSuccess) { + onContinueClick() +// { router.navigate(SettingsDirections.GoBack) } + } else { + onScanAgainClick() +// { router.navigate(SettingsDirections.PairingScan) } + } } - ConstraintLayout( modifier = Modifier .fillMaxHeight() + .padding(padding) .padding(horizontal = 16.dp) ) { val (content, pair) = createRefs() @@ -112,14 +134,14 @@ internal fun ResultContent( Text( text = stringResource(id = title), - style = MaterialTheme.typography.h6, + style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, modifier = Modifier ) Text( text = stringResource(id = description), - style = MaterialTheme.typography.body1, + style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, modifier = Modifier ) diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreenFactory.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreenFactory.kt deleted file mode 100644 index 935f37e7..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.browserextension.ui.pairing.progress - -import androidx.compose.runtime.Composable - -class PairingProgressScreenFactory { - - @Composable - fun create(extensionId: String) { - PairingProgressScreen(extensionId) - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressUiState.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressUiState.kt index be00f6bf..428c98c2 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressUiState.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressUiState.kt @@ -1,6 +1,6 @@ package com.twofasapp.browserextension.ui.pairing.progress -internal data class PairingProgressUiState( +data class PairingProgressUiState( val isPairing: Boolean = true, val isPairingSuccess: Boolean = false, val code: Int? = null diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt index 7c6c9602..d56ccf36 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/progress/PairingProgressViewModel.kt @@ -5,14 +5,14 @@ import com.twofasapp.base.BaseViewModel import com.twofasapp.base.dispatcher.Dispatchers import com.twofasapp.browserextension.domain.PairBrowserCase import com.twofasapp.browserextension.domain.RegisterMobileDeviceCase -import com.twofasapp.network.exception.BrowserAlreadyPairedException +import com.twofasapp.data.browserext.remote.exception.BrowserAlreadyPairedException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -internal class PairingProgressViewModel( +class PairingProgressViewModel( private val dispatchers: Dispatchers, private val registerMobileDeviceCase: RegisterMobileDeviceCase, private val pairBrowserCase: PairBrowserCase, diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreen.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreen.kt index fb1ff8a4..68ad1832 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreen.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreen.kt @@ -2,40 +2,41 @@ package com.twofasapp.browserextension.ui.pairing.scan import android.app.Activity import androidx.compose.foundation.clickable -import androidx.compose.material.Scaffold +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import com.twofasapp.resources.R -import com.twofasapp.design.compose.Toolbar import com.twofasapp.design.dialogs.InfoDialog -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.qrscanner.ui.QrScannerScreen -import org.koin.androidx.compose.get +import com.twofasapp.resources.R +import org.koin.androidx.compose.koinViewModel @Composable -internal fun PairingScanScreen( - viewModel: PairingScanViewModel = get(), - router: SettingsRouter = get(), +fun PairingScanScreen( + openPairingProgress: (String) -> Unit, + viewModel: PairingScanViewModel = koinViewModel(), ) { val uiState = viewModel.uiState.collectAsState() val activity = (LocalContext.current as? Activity) + var openSuccess by remember { mutableStateOf(true) } Scaffold( topBar = { - Toolbar( - title = stringResource(id = R.string.commons__scan_qr_code)) { - router.navigate(SettingsDirections.GoBack) - } + TwTopAppBar(titleText = stringResource(id = R.string.commons__scan_qr_code), modifier = Modifier.clickable { viewModel.pairMockedBrowser() }, showBackButton = true) } ) { padding -> QrScannerScreen() - if (uiState.value.isSuccess) { - router.navigate(SettingsDirections.PairingProgress(uiState.value.extensionId)) + if (uiState.value.isSuccess && openSuccess) { + openSuccess = false + openPairingProgress(uiState.value.extensionId) } if (uiState.value.showErrorDialog) { diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreenFactory.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreenFactory.kt deleted file mode 100644 index ef93f90e..00000000 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.browserextension.ui.pairing.scan - -import androidx.compose.runtime.Composable - -class PairingScanScreenFactory { - - @Composable - fun create() { - PairingScanScreen() - } -} \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanUiState.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanUiState.kt index c8c3ef1f..60531081 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanUiState.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanUiState.kt @@ -1,6 +1,6 @@ package com.twofasapp.browserextension.ui.pairing.scan -internal data class PairingScanUiState( +data class PairingScanUiState( val isSuccess: Boolean = false, val extensionId: String = "", val showErrorDialog: Boolean = false, diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanViewModel.kt b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanViewModel.kt index f4a3df07..856f1ea3 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanViewModel.kt +++ b/browserextension/src/main/java/com/twofasapp/browserextension/ui/pairing/scan/PairingScanViewModel.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -internal class PairingScanViewModel( +class PairingScanViewModel( private val scanQr: ScanQr, ) : BaseViewModel() { @@ -59,4 +59,14 @@ internal class PairingScanViewModel( } } + + fun pairMockedBrowser() { + viewModelScope.launch { + scanQr.publishResult( + ScanQr.Result( + "twofas_c://662699c0-dab0-4c3e-93a8-81dc31e24747" + ) + ) + } + } } diff --git a/build.gradle.kts b/build.gradle.kts index 2cd3901c..f3296d77 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20") - classpath("org.jetbrains.kotlin:kotlin-serialization:1.7.20") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}") + classpath("org.jetbrains.kotlin:kotlin-serialization:${libs.versions.kotlin.get()}") classpath("com.google.gms:google-services:4.3.14") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.2") } diff --git a/buildlogic/build.gradle.kts b/buildlogic/build.gradle.kts index 53b187d7..154feccf 100644 --- a/buildlogic/build.gradle.kts +++ b/buildlogic/build.gradle.kts @@ -15,8 +15,8 @@ java { } dependencies { - compileOnly("com.android.tools.build:gradle:8.0.0-alpha11") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20") + compileOnly("com.android.tools.build:gradle:${libs.versions.agp.get()}") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}") } gradlePlugin { diff --git a/buildlogic/src/main/java/com/twofasapp/buildlogic/extension/KotlinAndroid.kt b/buildlogic/src/main/java/com/twofasapp/buildlogic/extension/KotlinAndroid.kt index ac098e42..b13a6352 100644 --- a/buildlogic/src/main/java/com/twofasapp/buildlogic/extension/KotlinAndroid.kt +++ b/buildlogic/src/main/java/com/twofasapp/buildlogic/extension/KotlinAndroid.kt @@ -47,6 +47,8 @@ internal fun Project.applyKotlinAndroid( "-opt-in=kotlin.Experimental", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.lifecycle.compose.ExperimentalLifecycleComposeApi", + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true", ) } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 1ad9b7eb..bd3fc376 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -8,6 +8,6 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(libs.kotlinCoroutines) } \ No newline at end of file diff --git a/environment/src/main/java/com/twofasapp/environment/AppConfig.kt b/core/common/src/main/java/com/twofasapp/common/environment/AppBuild.kt similarity index 59% rename from environment/src/main/java/com/twofasapp/environment/AppConfig.kt rename to core/common/src/main/java/com/twofasapp/common/environment/AppBuild.kt index 4e8161ae..0d0ec63f 100644 --- a/environment/src/main/java/com/twofasapp/environment/AppConfig.kt +++ b/core/common/src/main/java/com/twofasapp/common/environment/AppBuild.kt @@ -1,10 +1,10 @@ -package com.twofasapp.environment +package com.twofasapp.common.environment -interface AppConfig { +interface AppBuild { val id: String - val isDebug: Boolean + val isDebuggable: Boolean val versionName: String val versionCode: Int val buildVariant: BuildVariant val deviceName: String -} +} \ No newline at end of file diff --git a/core/common/src/main/java/com/twofasapp/common/environment/BuildVariant.kt b/core/common/src/main/java/com/twofasapp/common/environment/BuildVariant.kt new file mode 100644 index 00000000..cf72b497 --- /dev/null +++ b/core/common/src/main/java/com/twofasapp/common/environment/BuildVariant.kt @@ -0,0 +1,7 @@ +package com.twofasapp.common.environment + +enum class BuildVariant { + Release, + ReleaseLocal, + Debug, +} \ No newline at end of file diff --git a/core/common/src/main/java/com/twofasapp/common/ktx/Base64.kt b/core/common/src/main/java/com/twofasapp/common/ktx/Base64.kt new file mode 100644 index 00000000..030b83c4 --- /dev/null +++ b/core/common/src/main/java/com/twofasapp/common/ktx/Base64.kt @@ -0,0 +1,79 @@ +package com.twofasapp.common.ktx + +import java.io.ByteArrayOutputStream + +fun String.encodeBase64ToString(): String = String(this.toByteArray().encodeBase64()) +fun String.encodeBase64ToByteArray(): ByteArray = this.toByteArray().encodeBase64() +fun ByteArray.encodeBase64ToString(): String = String(this.encodeBase64()) + +fun String.decodeBase64(): String = String(this.toByteArray().decodeBase64()) +fun String.decodeBase64ToByteArray(): ByteArray = this.toByteArray().decodeBase64() +fun ByteArray.decodeBase64ToString(): String = String(this.decodeBase64()) + +private fun ByteArray.encodeBase64(): ByteArray { + val table = (CharRange('A', 'Z') + CharRange('a', 'z') + CharRange('0', '9') + '+' + '/').toCharArray() + val output = ByteArrayOutputStream() + var padding = 0 + var position = 0 + while (position < this.size) { + var b = this[position].toInt() and 0xFF shl 16 and 0xFFFFFF + if (position + 1 < this.size) b = b or (this[position + 1].toInt() and 0xFF shl 8) else padding++ + if (position + 2 < this.size) b = b or (this[position + 2].toInt() and 0xFF) else padding++ + for (i in 0 until 4 - padding) { + val c = b and 0xFC0000 shr 18 + output.write(table[c].code) + b = b shl 6 + } + position += 3 + } + for (i in 0 until padding) { + output.write('='.code) + } + return output.toByteArray() +} + +private fun ByteArray.decodeBase64(): ByteArray { + val table = intArrayOf( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, + -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ) + + val output = ByteArrayOutputStream() + var position = 0 + while (position < this.size) { + var b: Int + if (table[this[position].toInt()] != -1) { + b = table[this[position].toInt()] and 0xFF shl 18 + } else { + position++ + continue + } + var count = 0 + if (position + 1 < this.size && table[this[position + 1].toInt()] != -1) { + b = b or (table[this[position + 1].toInt()] and 0xFF shl 12) + count++ + } + if (position + 2 < this.size && table[this[position + 2].toInt()] != -1) { + b = b or (table[this[position + 2].toInt()] and 0xFF shl 6) + count++ + } + if (position + 3 < this.size && table[this[position + 3].toInt()] != -1) { + b = b or (table[this[position + 3].toInt()] and 0xFF) + count++ + } + while (count > 0) { + val c = b and 0xFF0000 shr 16 + output.write(c.toChar().toInt()) + b = b shl 8 + count-- + } + position += 4 + } + return output.toByteArray() +} \ No newline at end of file diff --git a/core/common/src/main/java/com/twofasapp/common/ktx/CoroutinesKtx.kt b/core/common/src/main/java/com/twofasapp/common/ktx/CoroutinesKtx.kt new file mode 100644 index 00000000..39eb6ae1 --- /dev/null +++ b/core/common/src/main/java/com/twofasapp/common/ktx/CoroutinesKtx.kt @@ -0,0 +1,22 @@ +package com.twofasapp.common.ktx + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow + +inline fun runSafely(block: () -> T): Result = + try { + Result.success(block()) + } catch (cancellationException: CancellationException) { + throw cancellationException + } catch (exception: Exception) { + Result.failure(exception) + } + +fun tickerFlow(period: Long, initialDelay: Long = 0) = flow { + delay(initialDelay) + while (true) { + emit(Unit) + delay(period) + } +} \ No newline at end of file diff --git a/core/common/src/main/java/com/twofasapp/common/ktx/NavigationKtx.kt b/core/common/src/main/java/com/twofasapp/common/ktx/NavigationKtx.kt new file mode 100644 index 00000000..05e672f4 --- /dev/null +++ b/core/common/src/main/java/com/twofasapp/common/ktx/NavigationKtx.kt @@ -0,0 +1,9 @@ +package com.twofasapp.common.ktx + +import androidx.navigation.NavController + +fun NavController.clearGraphBackStack() { + currentBackStackEntry?.destination?.parent?.route?.let { currentGraphRoute -> + popBackStack(currentGraphRoute, inclusive = true) + } +} \ No newline at end of file diff --git a/core/common/src/main/java/com/twofasapp/common/ktx/StringKtx.kt b/core/common/src/main/java/com/twofasapp/common/ktx/StringKtx.kt new file mode 100644 index 00000000..2f419d65 --- /dev/null +++ b/core/common/src/main/java/com/twofasapp/common/ktx/StringKtx.kt @@ -0,0 +1,5 @@ +package com.twofasapp.common.ktx + +fun String.lowercaseFirstLetter(): String { + return first().lowercase().plus(substring(1, length)) +} \ No newline at end of file diff --git a/core/common/src/main/java/com/twofasapp/common/navigation/NavNode.kt b/core/common/src/main/java/com/twofasapp/common/navigation/NavNode.kt index 5a54824c..070710de 100644 --- a/core/common/src/main/java/com/twofasapp/common/navigation/NavNode.kt +++ b/core/common/src/main/java/com/twofasapp/common/navigation/NavNode.kt @@ -1,9 +1,15 @@ package com.twofasapp.common.navigation +import androidx.navigation.NamedNavArgument + interface NavNode { val path: String + val graph: NavGraph - fun route(graph: NavGraph): String { - return "${graph.route}/$path" - } + val route: String + get() = "${graph.route}/$path" +} + +fun String.withArg(arg: NamedNavArgument, value: T): String { + return replace("{${arg.name}}", value.toString()) } \ No newline at end of file diff --git a/core/common/src/main/java/com/twofasapp/common/time/TimeProvider.kt b/core/common/src/main/java/com/twofasapp/common/time/TimeProvider.kt new file mode 100644 index 00000000..3ef25bb7 --- /dev/null +++ b/core/common/src/main/java/com/twofasapp/common/time/TimeProvider.kt @@ -0,0 +1,9 @@ +package com.twofasapp.common.time + +import java.time.OffsetDateTime + +interface TimeProvider { + fun currentDateTimeUtc(): OffsetDateTime + fun systemCurrentTime(): Long + fun systemElapsedTime(): Long +} \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index 46dcf3a3..147ee753 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -10,6 +10,7 @@ android { dependencies { implementation(project(":core:common")) + implementation(project(":core:locale")) implementation(libs.core) implementation(libs.bundles.appCompat) diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/AppTheme.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/AppTheme.kt index 29197752..d54e2645 100644 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/AppTheme.kt +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/AppTheme.kt @@ -13,6 +13,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import com.twofasapp.designsystem.internal.LocalThemeColors +import com.twofasapp.designsystem.internal.ThemeColors +import com.twofasapp.designsystem.internal.ThemeColorsDark +import com.twofasapp.designsystem.internal.ThemeColorsLight @Composable fun MainAppTheme( @@ -31,37 +35,37 @@ fun MainAppTheme( } } - val colors: TwsColors = when (isSystemInDarkTheme()) { - true -> TwsColorsDark() - false -> TwsColorsLight() + val colors: ThemeColors = when (isSystemInDarkTheme()) { + true -> ThemeColorsDark() + false -> ThemeColorsLight() } val colorScheme: ColorScheme = when (isSystemInDarkTheme()) { true -> darkColorScheme( primary = colors.primary, - onPrimary = colors.onSurface, + onPrimary = colors.onSurfacePrimary, background = colors.background, - onBackground = colors.onSurface, + onBackground = colors.onSurfacePrimary, surface = colors.surface, - onSurface = colors.onSurface, + onSurface = colors.onSurfacePrimary, surfaceVariant = colors.surface, - onSurfaceVariant = colors.onSurface, + onSurfaceVariant = colors.onSurfacePrimary, ) false -> lightColorScheme( primary = colors.primary, - onPrimary = colors.onSurface, + onPrimary = colors.onSurfacePrimary, background = colors.background, - onBackground = colors.onSurface, + onBackground = colors.onSurfacePrimary, surface = colors.surface, - onSurface = colors.onSurface, + onSurface = colors.onSurfacePrimary, surfaceVariant = colors.surface, - onSurfaceVariant = colors.onSurface, + onSurfaceVariant = colors.onSurfacePrimary, ) } CompositionLocalProvider( - LocalTwsColors provides colors, + LocalThemeColors provides colors, ) { MaterialTheme( colorScheme = colorScheme, diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwIcons.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwIcons.kt new file mode 100644 index 00000000..3258d9b4 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwIcons.kt @@ -0,0 +1,37 @@ +package com.twofasapp.designsystem + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.res.painterResource + +@Immutable +@Stable +object TwIcons { + val Placeholder @Composable get() = painterResource(R.drawable.ic_placeholder) + val More @Composable get() = painterResource(R.drawable.ic_more) + val Home @Composable get() = painterResource(R.drawable.ic_home) + val ExternalLink @Composable get() = painterResource(R.drawable.ic_external_link) + val Notification @Composable get() = painterResource(R.drawable.ic_notification) + val Settings @Composable get() = painterResource(R.drawable.ic_settings) + val Edit @Composable get() = painterResource(R.drawable.ic_edit) + val Qr @Composable get() = painterResource(R.drawable.ic_qr) + val Write @Composable get() = painterResource(R.drawable.ic_write) + val Licenses @Composable get() = painterResource(R.drawable.ic_licenses) + val Lock @Composable get() = painterResource(R.drawable.ic_lock) + val LockOpen @Composable get() = painterResource(R.drawable.ic_lock_open) + val Share @Composable get() = painterResource(R.drawable.ic_share) + val Terms @Composable get() = painterResource(R.drawable.ic_terms) + val DragHandle @Composable get() = painterResource(R.drawable.ic_drag_handle) + val Add @Composable get() = painterResource(R.drawable.ic_add) + val CloudUpload @Composable get() = painterResource(R.drawable.ic_cloud_upload) + val Delete @Composable get() = painterResource(R.drawable.ic_delete) + val Extension @Composable get() = painterResource(R.drawable.ic_extension) + val Eye @Composable get() = painterResource(R.drawable.ic_eye) + val Favorite @Composable get() = painterResource(R.drawable.ic_favorite) + val FileUpload @Composable get() = painterResource(R.drawable.ic_file_upload) + val Info @Composable get() = painterResource(R.drawable.ic_info) + val Security @Composable get() = painterResource(R.drawable.ic_security) + val Support @Composable get() = painterResource(R.drawable.ic_support) + val Copy @Composable get() = painterResource(R.drawable.ic_copy) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwTheme.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwTheme.kt new file mode 100644 index 00000000..76033395 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwTheme.kt @@ -0,0 +1,31 @@ +package com.twofasapp.designsystem + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.luminance +import com.twofasapp.designsystem.internal.LocalThemeColors +import com.twofasapp.designsystem.internal.ThemeColors +import com.twofasapp.designsystem.internal.ThemeDimens +import com.twofasapp.designsystem.internal.ThemeShapes +import com.twofasapp.designsystem.internal.ThemeTypo + +object TwTheme { + val color: ThemeColors + @Composable + get() = LocalThemeColors.current + + val typo: ThemeTypo + @Composable + get() = ThemeTypo() + + val shape: ThemeShapes + @Composable + get() = ThemeShapes() + + val dimen: ThemeDimens + @Composable + get() = ThemeDimens() + + val isDark: Boolean + @Composable + get() = color.background.luminance() < 0.5 +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColors.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColors.kt deleted file mode 100644 index d0159c20..00000000 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColors.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.twofasapp.designsystem - -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color - -interface TwsColors { - val primary: Color - val background: Color - val surface: Color - val onSurface: Color - val onSurfaceDarker: Color - val buttonPrimary: Color - val onButtonPrimary: Color - val divider: Color -} - - -val LocalTwsColors = staticCompositionLocalOf { - TwsColorsLight() -} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsDark.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsDark.kt deleted file mode 100644 index b12ec8fe..00000000 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsDark.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.twofasapp.designsystem - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color - -@Immutable -data class TwsColorsDark( - override val primary: Color = Color(0xFFED1C24), - override val background: Color = Color(0xFF15161B), - override val surface: Color = Color(0xFF15161C), - override val onSurface: Color = Color(0xFFFFFFFF), - override val onSurfaceDarker: Color = Color(0xFF555555), - override val buttonPrimary: Color = primary, - override val onButtonPrimary: Color = Color(0xFFFFFFFF), - override val divider: Color = Color(0x0FFFFFFF), -) : TwsColors \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsLight.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsLight.kt deleted file mode 100644 index 88265f13..00000000 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsColorsLight.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.twofasapp.designsystem - -import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color - -@Immutable -data class TwsColorsLight( - override val primary: Color = Color(0xFFED1C24), - override val background: Color = Color(0xFFFFFFFF), - override val surface: Color = Color(0xFFFFFFFE), - override val onSurface: Color = Color(0xDD000000), - override val onSurfaceDarker: Color = Color(0xFF9E9E9E), - override val buttonPrimary: Color = primary, - override val onButtonPrimary: Color = Color(0xFFFFFFFF), - override val divider: Color = Color(0x14000000), -) : TwsColors \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTheme.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTheme.kt deleted file mode 100644 index 8dc19433..00000000 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTheme.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.twofasapp.designsystem - -import androidx.compose.runtime.Composable - -object TwsTheme { - val color: TwsColors - @Composable - get() = LocalTwsColors.current - - val typo: TwsTypo - @Composable - get() = TwsTypo - - val shape: TwsShape - @Composable - get() = TwsShape - - val dimen: TwsDimen - @Composable - get() = TwsDimen -} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTypo.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTypo.kt deleted file mode 100644 index ff1fe643..00000000 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsTypo.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.twofasapp.designsystem - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -@Immutable -@Stable -object TwsTypo { - val labelLarge = TextStyle( - fontFamily = FontFamily.SansSerif, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, - ) -} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Button.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Button.kt new file mode 100644 index 00000000..9005c267 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Button.kt @@ -0,0 +1,126 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme + +@Composable +fun TwButton( + text: String, + onClick: () -> Unit, + height: Dp = TwTheme.dimen.buttonHeight, + modifier: Modifier = Modifier, + style: TextStyle = TwTheme.typo.body2, + enabled: Boolean = true, +) { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = TwTheme.color.button, + contentColor = TwTheme.color.onButton, + ), + enabled = enabled, + modifier = modifier.height(height), + ) { + Text( + text = text, + style = style, + ) + } +} + +@Composable +fun TwOutlinedButton( + text: String, + onClick: () -> Unit, + height: Dp = TwTheme.dimen.buttonHeight, + modifier: Modifier = Modifier, + style: TextStyle = TwTheme.typo.body2, + enabled: Boolean = true, +) { + OutlinedButton( + onClick = onClick, + border = BorderStroke( + width = 1.dp, + color = TwTheme.color.primary, + ), + enabled = enabled, + modifier = modifier.height(height), + ) { + Text( + text = text, + style = style, + ) + } +} + +@Composable +fun TwTextButton( + text: String, + onClick: () -> Unit, + height: Dp = TwTheme.dimen.buttonHeight, + modifier: Modifier = Modifier, + style: TextStyle = TwTheme.typo.body2, + enabled: Boolean = true, +) { + TextButton( + onClick = onClick, + modifier = modifier.height(height), + enabled = enabled, + ) { + Text( + text = text, + style = style, + color = TwTheme.color.primary + ) + } +} + +@Composable +fun TwIconButton( + painter: Painter? = null, + contentDescription: String? = null, + onClick: () -> Unit = {}, + tint: Color? = null, + modifier: Modifier = Modifier, + iconModifier: Modifier = Modifier, + enabled: Boolean = true, + colors: IconButtonColors = IconButtonDefaults.iconButtonColors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable (() -> Unit)? = null, +) { + IconButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource + ) { + content?.invoke() ?: painter?.let { + Icon( + painter = it, + contentDescription = contentDescription, + modifier = iconModifier, + tint = tint ?: TwTheme.color.iconTint + ) + } + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/DropdownMenu.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/DropdownMenu.kt new file mode 100644 index 00000000..799f7700 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/DropdownMenu.kt @@ -0,0 +1,72 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.MenuItemColors +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme + +@Composable +fun TwDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + anchor: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit +) { + Box { + anchor() + + MaterialTheme( + shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp)), + ) { + DropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = Modifier + .widthIn(min = 160.dp) + .background(TwTheme.color.surface), + content = content, + ) + } + + } +} + +@Composable +fun TwDropdownMenuItem( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + enabled: Boolean = true, + colors: MenuItemColors = MenuDefaults.itemColors(), + contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + DropdownMenuItem( + text = { Text(text, modifier = Modifier.padding(start = 16.dp)) }, + onClick = onClick, + modifier = modifier, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + enabled = enabled, + colors = colors, + contentPadding = contentPadding, + interactionSource = interactionSource + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/HeaderItem.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/HeaderItem.kt new file mode 100644 index 00000000..044ad369 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/HeaderItem.kt @@ -0,0 +1,32 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun HeaderItem(text: String) { + Text( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Medium, + color = Color(0xFFD81F26), + fontSize = 14.sp, + ), + modifier = Modifier + .fillMaxWidth() + .padding( + start = 64.dp, + top = 16.dp, + end = 16.dp, + bottom = 12.dp, + ) + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Lazy.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Lazy.kt new file mode 100644 index 00000000..29308922 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Lazy.kt @@ -0,0 +1,27 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +fun LazyListState.isScrollingUp(): Boolean { + var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) } + var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) } + return remember(this) { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Modal.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Modal.kt new file mode 100644 index 00000000..1f22a49b --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Modal.kt @@ -0,0 +1,65 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme + +@Composable +fun ModalList( + content: @Composable () -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .background(TwTheme.color.surface) + .padding(vertical = 16.dp) + ) { + content() + } +} + +@Composable +fun ModalListItem( + text: String, + icon: Painter, + iconTint: Color = TwTheme.color.primary, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .background(TwTheme.color.surface) + .clickable { onClick() } + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically) { + + Icon(icon, null, tint = iconTint, modifier = Modifier.size(24.dp)) + + Spacer(Modifier.width(16.dp)) + + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/ModalBottomSheet.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/ModalBottomSheet.kt new file mode 100644 index 00000000..36272a22 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/ModalBottomSheet.kt @@ -0,0 +1,39 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetDefaults +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ModalBottomSheet( + sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden), + sheetContent: @Composable () -> Unit, + content: @Composable () -> Unit +) { + ModalBottomSheetLayout( + modifier = Modifier, + sheetState = sheetState, + sheetContent = { + Column(Modifier.navigationBarsPadding()) { + sheetContent() + } + }, + sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + sheetElevation = 2.dp, + sheetBackgroundColor = TwTheme.color.surface, + sheetContentColor = TwTheme.color.onSurfacePrimary, + scrimColor = ModalBottomSheetDefaults.scrimColor, + content = content, + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/composable/NavigationBar.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/NavigationBar.kt similarity index 53% rename from core/designsystem/src/main/java/com/twofasapp/designsystem/composable/NavigationBar.kt rename to core/designsystem/src/main/java/com/twofasapp/designsystem/common/NavigationBar.kt index 1c865128..88b82f0f 100644 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/composable/NavigationBar.kt +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/NavigationBar.kt @@ -1,6 +1,8 @@ -package com.twofasapp.designsystem.composable +package com.twofasapp.designsystem.common import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem @@ -10,26 +12,26 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp -import com.twofasapp.designsystem.TwsTheme +import com.twofasapp.designsystem.TwTheme @Composable -fun TwsNavigationBar( +fun TwNavigationBar( modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { NavigationBar( - containerColor = TwsTheme.color.background, - tonalElevation = 4.dp, + tonalElevation = 0.dp, modifier = modifier, content = content, ) } @Composable -fun RowScope.TwsNavigationBarItem( +fun RowScope.TwNavigationBarItem( text: String, icon: Painter, selected: Boolean, + showBadge: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -37,13 +39,22 @@ fun RowScope.TwsNavigationBarItem( selected = selected, onClick = onClick, label = { Text(text) }, - icon = { Icon(painter = icon, contentDescription = null) }, + icon = { + BadgedBox(badge = { + if (showBadge) { + Badge(containerColor = TwTheme.color.primary) + } + }) { + Icon(painter = icon, contentDescription = null) + } + + }, colors = NavigationBarItemDefaults.colors( - selectedIconColor = TwsTheme.color.primary, - selectedTextColor = TwsTheme.color.primary, - indicatorColor = TwsTheme.color.background, - unselectedIconColor = TwsTheme.color.onSurfaceDarker, - unselectedTextColor = TwsTheme.color.onSurfaceDarker, + selectedIconColor = TwTheme.color.primary, + selectedTextColor = TwTheme.color.primary, + indicatorColor = TwTheme.color.primaryIndicator, + unselectedIconColor = TwTheme.color.onSurfaceSecondary, + unselectedTextColor = TwTheme.color.onSurfaceSecondary, ), modifier = modifier, ) diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Progress.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Progress.kt new file mode 100644 index 00000000..f43fd704 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/Progress.kt @@ -0,0 +1,18 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun TwCircularProgressIndicator( + modifier: Modifier = Modifier, + color: Color = ProgressIndicatorDefaults.circularColor, + + ) { + CircularProgressIndicator(modifier.size(32.dp), color, 4.dp) +} diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/SimpleItem.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/SimpleItem.kt new file mode 100644 index 00000000..49c8f037 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/SimpleItem.kt @@ -0,0 +1,105 @@ +package com.twofasapp.designsystem.common + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.locale.TwLocale + +@Composable +fun SimpleItem( + title: String = "", + subtitle: String? = null, + image: Painter? = null, + icon: Painter? = null, + iconTint: Color = TwTheme.color.iconTint, + iconVisibleWhenNotSet: Boolean = true, + enabled: Boolean = true, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, +) { + Column( + modifier = modifier + .fillMaxWidth() + .heightIn(56.dp, Dp.Infinity) + .clickable(enabled) { onClick?.invoke() } + .padding( + horizontal = 16.dp, + vertical = 12.dp + ), + verticalArrangement = Arrangement.Center + ) { + + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + if (image != null) { + Image( + painter = image, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } + + if (icon != null) { + Icon( + painter = icon, + contentDescription = null, + tint = iconTint, + modifier = Modifier.size(24.dp), + ) + } + + if (image == null && icon == null && iconVisibleWhenNotSet) { + Spacer(modifier = Modifier.width(24.dp)) + } + + Spacer(modifier = Modifier.width(24.dp)) + + Text( + text = title, + style = MaterialTheme.typography.bodyMedium.copy(fontSize = 17.sp, color = TwTheme.color.onSurfacePrimary), + modifier = Modifier.weight(1f) + ) + } + + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 15.sp, color = TwTheme.color.onSurfaceSecondary), + modifier = Modifier + .fillMaxWidth() + .padding(start = 48.dp, top = 4.dp) + ) + } + } +} + +@Preview +@Composable +private fun Preview() { + SimpleItem( + title = TwLocale.strings.placeholder, + subtitle = TwLocale.strings.placeholderLong, + icon = TwIcons.Placeholder + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/common/TopAppBar.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/TopAppBar.kt new file mode 100644 index 00000000..b3dd0e62 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/common/TopAppBar.kt @@ -0,0 +1,196 @@ +package com.twofasapp.designsystem.common + + +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.twofasapp.designsystem.TwTheme + +@Composable +fun TwTopAppBar( + titleText: String? = null, + title: @Composable () -> Unit = {}, + containerColor: Color = TwTheme.color.background, + contentColor: Color = TwTheme.color.onSurfacePrimary, + actions: @Composable RowScope.() -> Unit = {}, + modifier: Modifier = Modifier, + scrollBehavior: TopAppBarScrollBehavior? = null, + showBackButton: Boolean = true, + onBackClick: (() -> Unit)? = null, + navigationIcon: (@Composable () -> Unit) = { + if (showBackButton) { + BackButton(onBackClick) + } + }, +) { + TopAppBar( + title = titleText?.let { + { + Text( + text = it, + color = contentColor, + style = TwTheme.typo.title + ) + } + } ?: title, + navigationIcon = navigationIcon, + actions = actions, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = containerColor, + scrolledContainerColor = containerColor, + navigationIconContentColor = contentColor, + titleContentColor = contentColor, + actionIconContentColor = contentColor + ), + scrollBehavior = scrollBehavior, + modifier = modifier + ) +} + +@Composable +fun TwCenterTopAppBar( + titleText: String? = null, + title: @Composable () -> Unit = {}, + containerColor: Color = TwTheme.color.background, + contentColor: Color = TwTheme.color.onSurfacePrimary, + actions: @Composable RowScope.() -> Unit = {}, + modifier: Modifier = Modifier, + scrollBehavior: TopAppBarScrollBehavior? = null, + showBackButton: Boolean = true, + onBackClick: (() -> Unit)? = null, + navigationIcon: (@Composable () -> Unit) = { + if (showBackButton) { + BackButton(onBackClick) + } + }, +) { + CenterAlignedTopAppBar( + title = titleText?.let { + { + Text( + text = it, + color = contentColor, + style = TwTheme.typo.title + ) + } + } ?: title, + navigationIcon = navigationIcon, + actions = actions, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = containerColor, + scrolledContainerColor = containerColor, + navigationIconContentColor = contentColor, + titleContentColor = contentColor, + actionIconContentColor = contentColor + ), + scrollBehavior = scrollBehavior, + modifier = modifier + ) +} + +@Composable +fun TwMediumTopAppBar( + titleText: String? = null, + title: @Composable () -> Unit = {}, + containerColor: Color = TwTheme.color.surface, + contentColor: Color = TwTheme.color.onSurfacePrimary, + actions: @Composable RowScope.() -> Unit = {}, + modifier: Modifier = Modifier, + scrollBehavior: TopAppBarScrollBehavior? = null, + showBackButton: Boolean = true, + onBackClick: (() -> Unit)? = null, + navigationIcon: (@Composable () -> Unit) = { + if (showBackButton) { + BackButton(onBackClick) + } + }, +) { + MediumTopAppBar( + title = titleText?.let { + { + Text( + text = it, + color = contentColor, + style = TwTheme.typo.title + ) + } + } ?: title, + navigationIcon = navigationIcon, + actions = actions, + colors = TopAppBarDefaults.mediumTopAppBarColors( + containerColor = containerColor, + scrolledContainerColor = containerColor, + navigationIconContentColor = contentColor, + titleContentColor = contentColor, + actionIconContentColor = contentColor + ), + scrollBehavior = scrollBehavior, + modifier = modifier + ) +} + +@Composable +fun TwLargeTopAppBar( + titleText: String? = null, + title: @Composable () -> Unit = {}, + containerColor: Color = TwTheme.color.surface, + contentColor: Color = TwTheme.color.onSurfacePrimary, + actions: @Composable RowScope.() -> Unit = {}, + modifier: Modifier = Modifier, + scrollBehavior: TopAppBarScrollBehavior? = null, + showBackButton: Boolean = true, + onBackClick: (() -> Unit)? = null, + navigationIcon: (@Composable () -> Unit) = { + if (showBackButton) { + BackButton(onBackClick) + } + }, +) { + LargeTopAppBar( + title = titleText?.let { + { + Text( + text = it, + color = contentColor, + style = TwTheme.typo.title + ) + } + } ?: title, + navigationIcon = navigationIcon, + actions = actions, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = containerColor, + scrolledContainerColor = containerColor, + navigationIconContentColor = contentColor, + titleContentColor = contentColor, + actionIconContentColor = contentColor + ), + scrollBehavior = scrollBehavior, + modifier = modifier, + ) +} + +@Composable +internal fun BackButton(onBackClick: (() -> Unit)? = null) { + val onBackDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + + IconButton(onClick = { onBackClick?.invoke() ?: onBackDispatcher?.onBackPressed() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null + ) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/composable/Button.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/composable/Button.kt deleted file mode 100644 index 4433342f..00000000 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/composable/Button.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.twofasapp.designsystem.composable - -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import com.twofasapp.designsystem.TwsTheme - -@Composable -fun TwsPrimaryButton( - text: String, - modifier: Modifier = Modifier, - style: TextStyle = TwsTheme.typo.labelLarge, - enabled: Boolean = true, - onClick: () -> Unit, -) { - Button( - onClick = onClick, - colors = ButtonDefaults.buttonColors( - containerColor = TwsTheme.color.buttonPrimary, - contentColor = TwsTheme.color.onButtonPrimary, - ), - shape = TwsTheme.shape.roundedButton, - enabled = enabled, - modifier = modifier.height(TwsTheme.dimen.buttonHeight), - ) { - Text( - text = text, - style = style, - ) - } -} - - -@Composable -fun TwsTextButton( - text: String, - modifier: Modifier = Modifier, - style: TextStyle = TwsTheme.typo.labelLarge, - enabled: Boolean = true, - onClick: () -> Unit, -) { - TextButton( - onClick = onClick, - modifier = modifier, - enabled = enabled, - ) { - Text( - text = text, - style = style, - color = TwsTheme.color.primary - ) - } -} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColors.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColors.kt new file mode 100644 index 00000000..df1d888e --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColors.kt @@ -0,0 +1,39 @@ +package com.twofasapp.designsystem.internal + +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +abstract class ThemeColors { + abstract val background: Color + abstract val surface: Color + abstract val surfaceVariant: Color + abstract val onSurfacePrimary: Color + abstract val onSurfaceSecondary: Color + abstract val onSurfaceTertiary: Color + abstract val primaryIndicator: Color + + val primary: Color = Color(0xFFED1C24) + val primaryDark: Color = Color(0xFFD81F26) + + val button: Color = primary + val onButton: Color = Color.White + + val divider: Color + get() = surfaceVariant + + val iconTint: Color + get() = onSurfaceSecondary + + val accentLightBlue: Color = Color(0xFF7F9CFF) + val accentIndigo: Color = Color(0xFF5E5CE6) + val accentPurple: Color = Color(0xFFD95DDC) + val accentTurquoise: Color = Color(0xFF2FCFBC) + val accentGreen: Color = Color(0xFF03BF38) + val accentRed: Color = Color(0xFFED1C24) + val accentOrange: Color = Color(0xFFFF7A00) + val accentYellow: Color = Color(0xFFFFBA0A) +} + +val LocalThemeColors = staticCompositionLocalOf { + ThemeColorsLight() +} diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsDark.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsDark.kt new file mode 100644 index 00000000..05c25a77 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsDark.kt @@ -0,0 +1,18 @@ +package com.twofasapp.designsystem.internal + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color + +@Immutable +@Stable +class ThemeColorsDark : ThemeColors() { + override val background: Color = Color(0xFF101116) + override val surface: Color = Color(0xFF1A1B21) + override val surfaceVariant: Color = Color(0xFF232323) + override val onSurfacePrimary: Color = Color(0xFFFFFFFF) + override val onSurfaceSecondary: Color = Color(0xFF636363) + override val onSurfaceTertiary: Color = Color(0xFF9E9E9E) + override val primaryIndicator: Color = Color(0xFF482227) + +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsLight.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsLight.kt new file mode 100644 index 00000000..db34d5eb --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeColorsLight.kt @@ -0,0 +1,17 @@ +package com.twofasapp.designsystem.internal + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color + +@Immutable +@Stable +class ThemeColorsLight : ThemeColors() { + override val background: Color = Color(0xFFFFFFFF) + override val surface: Color = Color(0xFFF9F9F9) + override val surfaceVariant: Color = Color(0xFFEEEEEE) + override val onSurfacePrimary: Color = Color(0xFF000000) + override val onSurfaceSecondary: Color = Color(0xFF9E9E9E) + override val onSurfaceTertiary: Color = Color(0xFF4C4C4C) + override val primaryIndicator: Color = Color(0xFFF8E2E3) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsDimen.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeDimens.kt similarity index 60% rename from core/designsystem/src/main/java/com/twofasapp/designsystem/TwsDimen.kt rename to core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeDimens.kt index 547ad93d..1feea847 100644 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsDimen.kt +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeDimens.kt @@ -1,4 +1,4 @@ -package com.twofasapp.designsystem +package com.twofasapp.designsystem.internal import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -6,8 +6,9 @@ import androidx.compose.ui.unit.dp @Immutable @Stable -object TwsDimen { - val buttonHeight = 42.dp +class ThemeDimens { + val buttonHeight = 44.dp + val buttonHeightSmall = 36.dp val radiusDefault = 12.dp val radiusSmall = 8.dp } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsShape.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeShapes.kt similarity index 72% rename from core/designsystem/src/main/java/com/twofasapp/designsystem/TwsShape.kt rename to core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeShapes.kt index 061c21f8..b6ec17aa 100644 --- a/core/designsystem/src/main/java/com/twofasapp/designsystem/TwsShape.kt +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeShapes.kt @@ -1,5 +1,6 @@ -package com.twofasapp.designsystem +package com.twofasapp.designsystem.internal +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -8,8 +9,9 @@ import androidx.compose.ui.unit.dp @Immutable @Stable -object TwsShape { +class ThemeShapes { val rect = RectangleShape + val circle = CircleShape val roundedButton = RoundedCornerShape(24.dp) val roundedDefault = RoundedCornerShape(12.dp) } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeTypo.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeTypo.kt new file mode 100644 index 00000000..25430a47 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/internal/ThemeTypo.kt @@ -0,0 +1,83 @@ +package com.twofasapp.designsystem.internal + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +@Immutable +@Stable +class ThemeTypo { + + val h1 = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Light, + fontSize = 42.sp, + lineHeight = 42.sp, + ) + + val h2 = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 32.sp, + ) + + val h3 = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + ) + + val title = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 26.sp, + ) + + val body1 = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + ) + + val body2 = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + ) + + val body3 = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 17.sp, + ) + + val body4 = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 13.sp, + lineHeight = 17.sp, + ) + + val caption = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + ) + + val subhead = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ContextKtx.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ContextKtx.kt new file mode 100644 index 00000000..e07a593d --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ContextKtx.kt @@ -0,0 +1,36 @@ +package com.twofasapp.designsystem.ktx + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.ContextWrapper +import android.widget.Toast +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocal +import androidx.core.app.ComponentActivity + +val CompositionLocal.currentActivity: ComponentActivity + @Composable + get() { + var context = this.current + + while (context is ContextWrapper) { + if (context is ComponentActivity) return context + context = context.baseContext + } + + error("No component activity") + } + +val LocalBackDispatcher + @Composable + get() = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher + +fun Context.copyToClipboard(text: String, label: String = "Text", toast: String = "Copied!") { + val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText(label, text) + clipboardManager.setPrimaryClip(clipData) + + Toast.makeText(this, toast, Toast.LENGTH_SHORT).show() +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ImageKtx.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ImageKtx.kt new file mode 100644 index 00000000..6b8e8021 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/ktx/ImageKtx.kt @@ -0,0 +1,27 @@ +package com.twofasapp.designsystem.ktx + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import java.io.IOException + +private val emptyBitmap: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + +fun Context.assetAsBitmap(fileName: String): Bitmap { + return try { + with(assets.open(fileName)) { + BitmapFactory.decodeStream(this) + } + } catch (e: IOException) { + emptyBitmap + } +} + +@Composable +fun assetAsBitmap(fileName: String): ImageBitmap { + return LocalContext.current.assetAsBitmap(fileName).asImageBitmap() +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/lazy/ListItem.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/lazy/ListItem.kt new file mode 100644 index 00000000..6927fc4c --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/lazy/ListItem.kt @@ -0,0 +1,35 @@ +package com.twofasapp.designsystem.lazy + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable + +interface ListItem { + val key: Any + val type: Any +} + +fun LazyListScope.listItem( + type: ListItem, + content: @Composable LazyItemScope.() -> Unit +) { + item( + key = type.key, + contentType = type.type, + content = content + ) +} + +fun LazyListScope.listItems( + items: List, + type: ((item: T) -> ListItem), + itemContent: @Composable LazyItemScope.(item: T) -> Unit +) { + items( + count = items.size, + key = { index: Int -> type(items[index]).key }, + contentType = { index: Int -> type(items[index]).type }, + ) { + itemContent(items[it]) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DetectReorder.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DetectReorder.kt new file mode 100644 index 00000000..d73f3419 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DetectReorder.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput + +fun Modifier.detectReorder(state: ReorderableState<*>) = + this.then( + Modifier.pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { + val down = awaitFirstDown(requireUnconsumed = false) + var drag: PointerInputChange? + var overSlop = Offset.Zero + do { + drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over -> + change.consume() + overSlop = over + } + } while (drag != null && !drag.isConsumed) + if (drag != null) { + state.interactions.trySend(StartDrag(down.id, overSlop)) + } + } + } + } + ) + + +fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = + this.then( + Modifier.pointerInput(Unit) { + forEachGesture { + val down = awaitPointerEventScope { + awaitFirstDown(requireUnconsumed = false) + } + awaitLongPressOrCancellation(down)?.also { + state.interactions.trySend(StartDrag(down.id)) + } + } + } + ) \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragCancelledAnimation.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragCancelledAnimation.kt new file mode 100644 index 00000000..af8b9d17 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragCancelledAnimation.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset + +interface DragCancelledAnimation { + suspend fun dragCancelled(position: ItemPosition, offset: Offset) + val position: ItemPosition? + val offset: Offset +} + +class NoDragCancelledAnimation : DragCancelledAnimation { + override suspend fun dragCancelled(position: ItemPosition, offset: Offset) {} + override val position: ItemPosition? = null + override val offset: Offset = Offset.Zero +} + +class SpringDragCancelledAnimation(private val stiffness: Float = Spring.StiffnessMediumLow) : DragCancelledAnimation { + private val animatable = Animatable(Offset.Zero, Offset.VectorConverter) + override val offset: Offset + get() = animatable.value + + override var position by mutableStateOf(null) + private set + + override suspend fun dragCancelled(position: ItemPosition, offset: Offset) { + this.position = position + animatable.snapTo(offset) + animatable.animateTo( + Offset.Zero, + spring(stiffness = stiffness, visibilityThreshold = Offset.VisibilityThreshold) + ) + this.position = null + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragGesture.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragGesture.kt new file mode 100644 index 00000000..24e90d98 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DragGesture.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed +import androidx.compose.ui.input.pointer.isOutOfBounds +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastAll +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastFirstOrNull +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.withTimeout + +// Copied from DragGestureDetector , as long the pointer api isn`t ready. + +internal suspend fun AwaitPointerEventScope.awaitPointerSlopOrCancellation( + pointerId: PointerId, + pointerType: PointerType, + onPointerSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit +): PointerInputChange? { + if (currentEvent.isPointerUp(pointerId)) { + return null // The pointer has already been lifted, so the gesture is canceled + } + var offset = Offset.Zero + val touchSlop = viewConfiguration.pointerSlop(pointerType) + + var pointer = pointerId + + while (true) { + val event = awaitPointerEvent() + val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null + if (dragEvent.isConsumed) { + return null + } else if (dragEvent.changedToUpIgnoreConsumed()) { + val otherDown = event.changes.fastFirstOrNull { it.pressed } + if (otherDown == null) { + // This is the last "up" + return null + } else { + pointer = otherDown.id + } + } else { + offset += dragEvent.positionChange() + val distance = offset.getDistance() + var acceptedDrag = false + if (distance >= touchSlop) { + val touchSlopOffset = offset / distance * touchSlop + onPointerSlopReached(dragEvent, offset - touchSlopOffset) + if (dragEvent.isConsumed) { + acceptedDrag = true + } else { + offset = Offset.Zero + } + } + + if (acceptedDrag) { + return dragEvent + } else { + awaitPointerEvent(PointerEventPass.Final) + if (dragEvent.isConsumed) { + return null + } + } + } + } +} + +internal suspend fun PointerInputScope.awaitLongPressOrCancellation( + initialDown: PointerInputChange +): PointerInputChange? { + var longPress: PointerInputChange? = null + var currentDown = initialDown + val longPressTimeout = viewConfiguration.longPressTimeoutMillis + return try { + // wait for first tap up or long press + withTimeout(longPressTimeout) { + awaitPointerEventScope { + var finished = false + while (!finished) { + val event = awaitPointerEvent(PointerEventPass.Main) + if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) { + // All pointers are up + finished = true + } + + if ( + event.changes.fastAny { + it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding) + } + ) { + finished = true // Canceled + } + + // Check for cancel by position consumption. We can look on the Final pass of + // the existing pointer event because it comes after the Main pass we checked + // above. + val consumeCheck = awaitPointerEvent(PointerEventPass.Final) + if (consumeCheck.changes.fastAny { it.isConsumed }) { + finished = true + } + if (!event.isPointerUp(currentDown.id)) { + longPress = event.changes.fastFirstOrNull { it.id == currentDown.id } + } else { + val newPressed = event.changes.fastFirstOrNull { it.pressed } + if (newPressed != null) { + currentDown = newPressed + longPress = currentDown + } else { + // should technically never happen as we checked it above + finished = true + } + } + } + } + } + null + } catch (_: TimeoutCancellationException) { + longPress ?: initialDown + } +} + +private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean = + changes.fastFirstOrNull { it.id == pointerId }?.pressed != true + +// This value was determined using experiments and common sense. +// We can't use zero slop, because some hypothetical desktop/mobile devices can send +// pointer events with a very high precision (but I haven't encountered any that send +// events with less than 1px precision) +private val mouseSlop = 0.125.dp +private val defaultTouchSlop = 18.dp // The default touch slop on Android devices +private val mouseToTouchSlopRatio = mouseSlop / defaultTouchSlop + +// TODO(demin): consider this as part of ViewConfiguration class after we make *PointerSlop* +// functions public (see the comment at the top of the file). +// After it will be a public API, we should get rid of `touchSlop / 144` and return absolute +// value 0.125.dp.toPx(). It is not possible right now, because we can't access density. +private fun ViewConfiguration.pointerSlop(pointerType: PointerType): Float { + return when (pointerType) { + PointerType.Mouse -> touchSlop * mouseToTouchSlopRatio + else -> touchSlop + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DraggedItem.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DraggedItem.kt new file mode 100644 index 00000000..9d2703fa --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/DraggedItem.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2021 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.zIndex + + +fun Modifier.draggedItem( + offset: Float?, + orientation: Orientation = Orientation.Vertical, +): Modifier = composed { + Modifier + .zIndex(offset?.let { 1f } ?: 0f) + .graphicsLayer { + with(offset ?: 0f) { + if (orientation == Orientation.Vertical) { + translationY = this + } else { + translationX = this + } + } + shadowElevation = offset?.let { 8f } ?: 0f + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ItemPosition.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ItemPosition.kt new file mode 100644 index 00000000..b082aa69 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ItemPosition.kt @@ -0,0 +1,3 @@ +package org.burnoutcrew.reorderable + +data class ItemPosition(val index: Int, val key: Any?) \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Move.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Move.kt new file mode 100644 index 00000000..4ed97078 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Move.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2021 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +fun MutableList.move(fromIdx: Int, toIdx: Int) { + when { + fromIdx == toIdx -> { + return + } + toIdx > fromIdx -> { + for (i in fromIdx until toIdx) { + this[i] = this[i + 1].also { this[i + 1] = this[i] } + } + } + else -> { + for (i in fromIdx downTo toIdx + 1) { + this[i] = this[i - 1].also { this[i - 1] = this[i] } + } + } + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Reorderable.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Reorderable.kt new file mode 100644 index 00000000..e9d6d076 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/Reorderable.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.foundation.gestures.drag +import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.changedToUp +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.util.fastFirstOrNull + +fun Modifier.reorderable( + state: ReorderableState<*> +) = then( + Modifier.pointerInput(Unit) { + forEachGesture { + val dragStart = state.interactions.receive() + val down = awaitPointerEventScope { + currentEvent.changes.fastFirstOrNull { it.id == dragStart.id } + } + if (down != null && state.onDragStart(down.position.x.toInt(), down.position.y.toInt())) { + dragStart.offset?.apply { + state.onDrag(x.toInt(), y.toInt()) + } + detectDrag( + down.id, + onDragEnd = { + state.onDragCanceled() + }, + onDragCancel = { + state.onDragCanceled() + }, + onDrag = { change, dragAmount -> + change.consume() + state.onDrag(dragAmount.x.toInt(), dragAmount.y.toInt()) + }) + } + } + }) + +internal suspend fun PointerInputScope.detectDrag( + down: PointerId, + onDragEnd: () -> Unit = { }, + onDragCancel: () -> Unit = { }, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit, +) { + awaitPointerEventScope { + if ( + drag(down) { + onDrag(it, it.positionChange()) + it.consume() + } + ) { + // consume up if we quit drag gracefully with the up + currentEvent.changes.forEach { + if (it.changedToUp()) it.consume() + } + onDragEnd() + } else { + onDragCancel() + } + } +} + +internal data class StartDrag(val id: PointerId, val offset: Offset? = null) \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableItem.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableItem.kt new file mode 100644 index 00000000..bbc95d2f --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableItem.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.grid.LazyGridItemScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.zIndex + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LazyItemScope.ReorderableItem( + reorderableState: ReorderableState<*>, + key: Any?, + modifier: Modifier = Modifier, + index: Int? = null, + orientationLocked: Boolean = true, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit +) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItemPlacement(), orientationLocked, index, content) + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LazyGridItemScope.ReorderableItem( + reorderableState: ReorderableState<*>, + key: Any?, + modifier: Modifier = Modifier, + index: Int? = null, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit +) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItemPlacement(), false, index, content) + +@Composable +fun ReorderableItem( + state: ReorderableState<*>, + key: Any?, + modifier: Modifier = Modifier, + defaultDraggingModifier: Modifier = Modifier, + orientationLocked: Boolean = true, + index: Int? = null, + content: @Composable BoxScope.(isDragging: Boolean) -> Unit +) { + val isDragging = if (index != null) { + index == state.draggingItemIndex + } else { + key == state.draggingItemKey + } + val draggingModifier = + if (isDragging) { + Modifier + .zIndex(1f) + .graphicsLayer { + translationX = if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f + translationY = if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f + } + } else { + val cancel = if (index != null) { + index == state.dragCancelledAnimation.position?.index + } else { + key == state.dragCancelledAnimation.position?.key + } + if (cancel) { + Modifier.zIndex(1f) + .graphicsLayer { + translationX = if (!orientationLocked || !state.isVerticalScroll) state.dragCancelledAnimation.offset.x else 0f + translationY = if (!orientationLocked || state.isVerticalScroll) state.dragCancelledAnimation.offset.y else 0f + } + } else { + defaultDraggingModifier + } + } + Box(modifier = modifier.then(draggingModifier)) { + content(isDragging) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyGridState.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyGridState.kt new file mode 100644 index 00000000..b803861e --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyGridState.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope + +@Composable +fun rememberReorderableLazyGridState( + onMove: (ItemPosition, ItemPosition) -> Unit, + gridState: LazyGridState = rememberLazyGridState(), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + maxScrollPerFrame: Dp = 20.dp, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() +): ReorderableLazyGridState { + val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() } + val scope = rememberCoroutineScope() + val state = remember(gridState) { + ReorderableLazyGridState(gridState, scope, maxScroll, onMove, canDragOver, onDragEnd, dragCancelledAnimation) + } + LaunchedEffect(state) { + state.visibleItemsChanged() + .collect { state.onDrag(0, 0) } + } + + LaunchedEffect(state) { + while (true) { + val diff = state.scrollChannel.receive() + gridState.scrollBy(diff) + } + } + return state +} + +class ReorderableLazyGridState( + val gridState: LazyGridState, + scope: CoroutineScope, + maxScrollPerFrame: Float, + onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() +) : ReorderableState(scope, maxScrollPerFrame, onMove, canDragOver, onDragEnd, dragCancelledAnimation) { + override val isVerticalScroll: Boolean + get() = gridState.layoutInfo.orientation == Orientation.Vertical + override val LazyGridItemInfo.left: Int + get() = offset.x + override val LazyGridItemInfo.right: Int + get() = offset.x + size.width + override val LazyGridItemInfo.top: Int + get() = offset.y + override val LazyGridItemInfo.bottom: Int + get() = offset.y + size.height + override val LazyGridItemInfo.width: Int + get() = size.width + override val LazyGridItemInfo.height: Int + get() = size.height + override val LazyGridItemInfo.itemIndex: Int + get() = index + override val LazyGridItemInfo.itemKey: Any + get() = key + override val visibleItemsInfo: List + get() = gridState.layoutInfo.visibleItemsInfo + override val viewportStartOffset: Int + get() = gridState.layoutInfo.viewportStartOffset + override val viewportEndOffset: Int + get() = gridState.layoutInfo.viewportEndOffset + override val firstVisibleItemIndex: Int + get() = gridState.firstVisibleItemIndex + override val firstVisibleItemScrollOffset: Int + get() = gridState.firstVisibleItemScrollOffset + + override suspend fun scrollToItem(index: Int, offset: Int) { + gridState.scrollToItem(index, offset) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyListState.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyListState.kt new file mode 100644 index 00000000..a9dae61a --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableLazyListState.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope + + +@Composable +fun rememberReorderableLazyListState( + onMove: (ItemPosition, ItemPosition) -> Unit, + listState: LazyListState = rememberLazyListState(), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + maxScrollPerFrame: Dp = 20.dp, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() +): ReorderableLazyListState { + val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() } + val scope = rememberCoroutineScope() + val state = remember(listState) { + ReorderableLazyListState(listState, scope, maxScroll, onMove, canDragOver, onDragEnd, dragCancelledAnimation) + } + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + LaunchedEffect(state) { + state.visibleItemsChanged() + .collect { state.onDrag(0, 0) } + } + + LaunchedEffect(state) { + var reverseDirection = !listState.layoutInfo.reverseLayout + if (isRtl && listState.layoutInfo.orientation != Orientation.Vertical) { + reverseDirection = !reverseDirection + } + val direction = if (reverseDirection) 1f else -1f + while (true) { + val diff = state.scrollChannel.receive() + listState.scrollBy(diff * direction) + } + } + return state +} + +class ReorderableLazyListState( + val listState: LazyListState, + scope: CoroutineScope, + maxScrollPerFrame: Float, + onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), + canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null, + onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null, + dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation() +) : ReorderableState( + scope, + maxScrollPerFrame, + onMove, + canDragOver, + onDragEnd, + dragCancelledAnimation +) { + override val isVerticalScroll: Boolean + get() = listState.layoutInfo.orientation == Orientation.Vertical + override val LazyListItemInfo.left: Int + get() = when { + isVerticalScroll -> 0 + listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.width - offset - size + else -> offset + } + override val LazyListItemInfo.top: Int + get() = when { + !isVerticalScroll -> 0 + listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.height - offset - size + else -> offset + } + override val LazyListItemInfo.right: Int + get() = when { + isVerticalScroll -> 0 + listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.width - offset + else -> offset + size + } + override val LazyListItemInfo.bottom: Int + get() = when { + !isVerticalScroll -> 0 + listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.height - offset + else -> offset + size + } + override val LazyListItemInfo.width: Int + get() = if (isVerticalScroll) 0 else size + override val LazyListItemInfo.height: Int + get() = if (isVerticalScroll) size else 0 + override val LazyListItemInfo.itemIndex: Int + get() = index + override val LazyListItemInfo.itemKey: Any + get() = key + override val visibleItemsInfo: List + get() = listState.layoutInfo.visibleItemsInfo + override val viewportStartOffset: Int + get() = listState.layoutInfo.viewportStartOffset + override val viewportEndOffset: Int + get() = listState.layoutInfo.viewportEndOffset + override val firstVisibleItemIndex: Int + get() = listState.firstVisibleItemIndex + override val firstVisibleItemScrollOffset: Int + get() = listState.firstVisibleItemScrollOffset + + override suspend fun scrollToItem(index: Int, offset: Int) { + listState.scrollToItem(index, offset) + } + + override fun onDragStart(offsetX: Int, offsetY: Int): Boolean = + if (isVerticalScroll) { + super.onDragStart(0, offsetY) + } else { + super.onDragStart(offsetX, 0) + } + + override fun findTargets(x: Int, y: Int, selected: LazyListItemInfo) = + if (isVerticalScroll) { + super.findTargets(0, y, selected) + } else { + super.findTargets(x, 0, selected) + } + + override fun chooseDropItem( + draggedItemInfo: LazyListItemInfo?, + items: List, + curX: Int, + curY: Int + ) = + if (isVerticalScroll) { + super.chooseDropItem(draggedItemInfo, items, 0, curY) + } else { + super.chooseDropItem(draggedItemInfo, items, curX, 0) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableState.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableState.kt new file mode 100644 index 00000000..f4b5c6e9 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/reorderable/ReorderableState.kt @@ -0,0 +1,348 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.util.fastForEach +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlin.math.absoluteValue +import kotlin.math.min +import kotlin.math.sign + + +abstract class ReorderableState( + private val scope: CoroutineScope, + private val maxScrollPerFrame: Float, + private val onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit), + private val canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)?, + private val onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))?, + val dragCancelledAnimation: DragCancelledAnimation +) { + var draggingItemIndex by mutableStateOf(null) + private set + val draggingItemKey: Any? + get() = selected?.itemKey + protected abstract val T.left: Int + protected abstract val T.top: Int + protected abstract val T.right: Int + protected abstract val T.bottom: Int + protected abstract val T.width: Int + protected abstract val T.height: Int + protected abstract val T.itemIndex: Int + protected abstract val T.itemKey: Any + protected abstract val visibleItemsInfo: List + protected abstract val firstVisibleItemIndex: Int + protected abstract val firstVisibleItemScrollOffset: Int + protected abstract val viewportStartOffset: Int + protected abstract val viewportEndOffset: Int + internal val interactions = Channel() + internal val scrollChannel = Channel() + val draggingItemLeft: Float + get() = draggingLayoutInfo?.let { item -> + (selected?.left ?: 0) + draggingDelta.x - item.left + } ?: 0f + val draggingItemTop: Float + get() = draggingLayoutInfo?.let { item -> + (selected?.top ?: 0) + draggingDelta.y - item.top + } ?: 0f + abstract val isVerticalScroll: Boolean + private val draggingLayoutInfo: T? + get() = visibleItemsInfo + .firstOrNull { it.itemIndex == draggingItemIndex } + private var draggingDelta by mutableStateOf(Offset.Zero) + private var selected by mutableStateOf(null) + private var autoscroller: Job? = null + private val targets = mutableListOf() + private val distances = mutableListOf() + + protected abstract suspend fun scrollToItem(index: Int, offset: Int) + + @OptIn(ExperimentalCoroutinesApi::class) + internal fun visibleItemsChanged() = + snapshotFlow { draggingItemIndex != null } + .flatMapLatest { if (it) snapshotFlow { visibleItemsInfo } else flowOf(null) } + .filterNotNull() + .distinctUntilChanged { old, new -> old.firstOrNull()?.itemIndex == new.firstOrNull()?.itemIndex && old.count() == new.count() } + + internal open fun onDragStart(offsetX: Int, offsetY: Int): Boolean { + val x: Int + val y: Int + if (isVerticalScroll) { + x = offsetX + y = offsetY + viewportStartOffset + } else { + x = offsetX + viewportStartOffset + y = offsetY + } + return visibleItemsInfo + .firstOrNull { x in it.left..it.right && y in it.top..it.bottom } + ?.also { + selected = it + draggingItemIndex = it.itemIndex + } != null + } + + internal fun onDragCanceled() { + val dragIdx = draggingItemIndex + if (dragIdx != null) { + val position = ItemPosition(dragIdx, selected?.itemKey) + val offset = Offset(draggingItemLeft, draggingItemTop) + scope.launch { + dragCancelledAnimation.dragCancelled(position, offset) + } + } + val startIndex = selected?.itemIndex + val endIndex = draggingItemIndex + selected = null + draggingDelta = Offset.Zero + draggingItemIndex = null + cancelAutoScroll() + onDragEnd?.apply { + if (startIndex != null && endIndex != null) { + invoke(startIndex, endIndex) + } + } + } + + internal fun onDrag(offsetX: Int, offsetY: Int) { + val selected = selected ?: return + draggingDelta = Offset(draggingDelta.x + offsetX, draggingDelta.y + offsetY) + val draggingItem = draggingLayoutInfo ?: return + val startOffset = draggingItem.top + draggingItemTop + val startOffsetX = draggingItem.left + draggingItemLeft + chooseDropItem( + draggingItem, + findTargets(draggingDelta.x.toInt(), draggingDelta.y.toInt(), selected), + startOffsetX.toInt(), + startOffset.toInt() + )?.also { targetItem -> + if (targetItem.itemIndex == firstVisibleItemIndex || draggingItem.itemIndex == firstVisibleItemIndex) { + scope.launch { + onMove.invoke( + ItemPosition(draggingItem.itemIndex, draggingItem.itemKey), + ItemPosition(targetItem.itemIndex, targetItem.itemKey) + ) + scrollToItem(firstVisibleItemIndex, firstVisibleItemScrollOffset) + } + } else { + onMove.invoke( + ItemPosition(draggingItem.itemIndex, draggingItem.itemKey), + ItemPosition(targetItem.itemIndex, targetItem.itemKey) + ) + } + draggingItemIndex = targetItem.itemIndex + } + + with(calcAutoScrollOffset(0, maxScrollPerFrame)) { + if (this != 0f) autoscroll(this) + } + } + + private fun autoscroll(scrollOffset: Float) { + if (scrollOffset != 0f) { + if (autoscroller?.isActive == true) { + return + } + autoscroller = scope.launch { + var scroll = scrollOffset + var start = 0L + while (scroll != 0f && autoscroller?.isActive == true) { + withFrameMillis { + if (start == 0L) { + start = it + } else { + scroll = calcAutoScrollOffset(it - start, maxScrollPerFrame) + } + } + scrollChannel.trySend(scroll) + } + } + } else { + cancelAutoScroll() + } + } + + private fun cancelAutoScroll() { + autoscroller?.cancel() + autoscroller = null + } + + protected open fun findTargets(x: Int, y: Int, selected: T): List { + targets.clear() + distances.clear() + val left = x + selected.left + val right = x + selected.right + val top = y + selected.top + val bottom = y + selected.bottom + val centerX = (left + right) / 2 + val centerY = (top + bottom) / 2 + visibleItemsInfo.fastForEach { item -> + if ( + item.itemIndex == draggingItemIndex + || item.bottom < top + || item.top > bottom + || item.right < left + || item.left > right + ) { + return@fastForEach + } + if (canDragOver?.invoke( + ItemPosition(item.itemIndex, item.itemKey), + ItemPosition(selected.itemIndex, selected.itemKey) + ) != false + ) { + val dx = (centerX - (item.left + item.right) / 2).absoluteValue + val dy = (centerY - (item.top + item.bottom) / 2).absoluteValue + val dist = dx * dx + dy * dy + var pos = 0 + for (j in targets.indices) { + if (dist > distances[j]) { + pos++ + } else { + break + } + } + targets.add(pos, item) + distances.add(pos, dist) + } + } + return targets + } + + protected open fun chooseDropItem(draggedItemInfo: T?, items: List, curX: Int, curY: Int): T? { + if (draggedItemInfo == null) { + return if (draggingItemIndex != null) items.lastOrNull() else null + } + var target: T? = null + var highScore = -1 + val right = curX + draggedItemInfo.width + val bottom = curY + draggedItemInfo.height + val dx = curX - draggedItemInfo.left + val dy = curY - draggedItemInfo.top + + items.fastForEach { item -> + if (dx > 0) { + val diff = item.right - right + if (diff < 0 && item.right > draggedItemInfo.right) { + val score = diff.absoluteValue + if (score > highScore) { + highScore = score + target = item + } + } + } + if (dx < 0) { + val diff = item.left - curX + if (diff > 0 && item.left < draggedItemInfo.left) { + val score = diff.absoluteValue + if (score > highScore) { + highScore = score + target = item + } + } + } + if (dy < 0) { + val diff = item.top - curY + if (diff > 0 && item.top < draggedItemInfo.top) { + val score = diff.absoluteValue + if (score > highScore) { + highScore = score + target = item + } + } + } + if (dy > 0) { + val diff = item.bottom - bottom + if (diff < 0 && item.bottom > draggedItemInfo.bottom) { + val score = diff.absoluteValue + if (score > highScore) { + highScore = score + target = item + } + } + } + } + return target + } + + private fun calcAutoScrollOffset(time: Long, maxScroll: Float): Float { + val draggingItem = draggingLayoutInfo ?: return 0f + val startOffset: Float + val endOffset: Float + val delta: Float + if (isVerticalScroll) { + startOffset = draggingItem.top + draggingItemTop + endOffset = startOffset + draggingItem.height + delta = draggingDelta.y + } else { + startOffset = draggingItem.left + draggingItemLeft + endOffset = startOffset + draggingItem.width + delta = draggingDelta.x + } + return when { + delta > 0 -> + (endOffset - viewportEndOffset).coerceAtLeast(0f) + delta < 0 -> + (startOffset - viewportStartOffset).coerceAtMost(0f) + else -> 0f + } + .let { interpolateOutOfBoundsScroll((endOffset - startOffset).toInt(), it, time, maxScroll) } + } + + + companion object { + private const val ACCELERATION_LIMIT_TIME_MS: Long = 1500 + private val EaseOutQuadInterpolator: (Float) -> (Float) = { + val t = 1 - it + 1 - t * t * t * t + } + private val EaseInQuintInterpolator: (Float) -> (Float) = { + it * it * it * it * it + } + + private fun interpolateOutOfBoundsScroll( + viewSize: Int, + viewSizeOutOfBounds: Float, + time: Long, + maxScroll: Float, + ): Float { + if (viewSizeOutOfBounds == 0f) return 0f + val outOfBoundsRatio = min(1f, 1f * viewSizeOutOfBounds.absoluteValue / viewSize) + val cappedScroll = sign(viewSizeOutOfBounds) * maxScroll * EaseOutQuadInterpolator(outOfBoundsRatio) + val timeRatio = if (time > ACCELERATION_LIMIT_TIME_MS) 1f else time.toFloat() / ACCELERATION_LIMIT_TIME_MS + return (cappedScroll * EaseInQuintInterpolator(timeRatio)).let { + if (it == 0f) { + if (viewSizeOutOfBounds > 0) 1f else -1f + } else { + it + } + } + } + } +} diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/screen/CommonContent.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/screen/CommonContent.kt new file mode 100644 index 00000000..32722227 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/screen/CommonContent.kt @@ -0,0 +1,120 @@ +package com.twofasapp.designsystem.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwButton +import com.twofasapp.locale.TwLocale + +@Composable +fun CommonContent( + image: Painter? = null, + titleText: String? = null, + descriptionText: String? = null, + ctaPrimaryText: String? = null, + ctaPrimaryClick: () -> Unit = {}, + title: @Composable (() -> Unit)? = null, + description: @Composable (() -> Unit)? = null, + cta: @Composable (() -> Unit)? = null, + modifier: Modifier = Modifier, +) { + Column(modifier) { + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + if (image != null) { + Image(painter = image, contentDescription = null, Modifier.height(120.dp)) + Spacer(Modifier.height(24.dp)) + } + + if (titleText != null) { + CommonContentTitle(text = titleText) + } + + title?.invoke() + + Spacer(Modifier.height(16.dp)) + + if (descriptionText != null) { + CommonContentDescription(text = descriptionText) + } + + description?.invoke() + + Spacer(Modifier.height(64.dp)) + } + + if (ctaPrimaryText != null) { + TwButton( + text = ctaPrimaryText, + onClick = ctaPrimaryClick, + modifier = Modifier.fillMaxWidth() + ) + } + + cta?.invoke() + } +} + +@Composable +fun CommonContentTitle(text: String) { + Text( + text = text, + style = TwTheme.typo.title, + color = TwTheme.color.onSurfacePrimary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) +} + +@Composable +fun CommonContentDescription(text: String) { + Text( + text = text, + style = TwTheme.typo.body1, + color = TwTheme.color.onSurfacePrimary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) +} + + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + CommonContent( + image = TwIcons.Placeholder, + titleText = TwLocale.strings.placeholder, + descriptionText = TwLocale.strings.placeholderLong, + ctaPrimaryText = TwLocale.strings.placeholder, + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) +} + diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/Service.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/Service.kt new file mode 100644 index 00000000..1638a67f --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/Service.kt @@ -0,0 +1,82 @@ +package com.twofasapp.designsystem.service + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwIconButton + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun Service( + state: ServiceState, + style: ServiceStyle, + isInEditMode: Boolean = false, + containerColor: Color = TwTheme.color.background, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onLongClick: () -> Unit = {}, +) { + if (isInEditMode) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(TwTheme.shape.roundedDefault) + .background(containerColor) + .combinedClickable( + onClick = { onClick() }, + onLongClick = { onLongClick() }, + ) + .border(1.dp, TwTheme.color.surfaceVariant, TwTheme.shape.roundedDefault) + .padding(start = 21.dp) + ) { + ServiceNoCode( + name = state.name, + info = state.info, + imageType = state.imageType, + iconLight = state.iconLight, + iconDark = state.iconDark, + labelText = state.labelText, + labelColor = state.labelColor, + imageSize = 36.dp, + containerColor = containerColor, + modifier = Modifier, + ) { + TwIconButton(TwIcons.DragHandle, enabled = false) + } + } + } else { + when (style) { + ServiceStyle.Normal -> { + ServiceNormal( + state = state, + containerColor = containerColor, + modifier = modifier, + onClick = onClick, + onLongClick = onLongClick, + ) + } + + ServiceStyle.Modal -> { + ServiceModal( + state = state, + containerColor = containerColor, + modifier = modifier, + ) + } + + ServiceStyle.Compact -> {} + } + + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceImageType.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceImageType.kt new file mode 100644 index 00000000..5e45b7ff --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceImageType.kt @@ -0,0 +1,3 @@ +package com.twofasapp.designsystem.service + +enum class ServiceImageType { Icon, Label } \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceModal.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceModal.kt new file mode 100644 index 00000000..9fd8f20e --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceModal.kt @@ -0,0 +1,107 @@ +package com.twofasapp.designsystem.service + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.service.component.ServiceCode +import com.twofasapp.designsystem.service.component.ServiceImage +import com.twofasapp.designsystem.service.component.ServiceInfo +import com.twofasapp.designsystem.service.component.ServiceName +import com.twofasapp.designsystem.service.component.ServiceTimer + +@Composable +internal fun ServiceModal( + state: ServiceState, + containerColor: Color = TwTheme.color.background, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(containerColor) + .padding(top = 24.dp, bottom = 16.dp) + .padding(horizontal = 8.dp) + ) { + ServiceName( + text = state.name, + style = TwTheme.typo.title, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + ServiceInfo( + text = state.info, + style = TwTheme.typo.body1, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.width(16.dp)) + + ServiceImage( + type = state.imageType, + iconLight = state.iconLight, + iconDark = state.iconDark, + labelText = state.labelText, + labelColor = state.labelColor + ) + + Spacer(Modifier.width(16.dp)) + + ServiceCode( + code = state.code, + nextCode = state.nextCode, + modifier = Modifier.weight(1f) + ) + + Spacer(Modifier.width(16.dp)) + + ServiceTimer( + timer = state.timer, + progress = state.progress + ) + + Spacer(Modifier.width(12.dp)) + } + } + +} + + +@Preview +@Composable +private fun Preview() { + Service( + state = ServiceState( + name = "Service Name", + info = "Additional Info", + code = "123456", + nextCode = "456789", + timer = 15, + progress = .5f, + imageType = ServiceImageType.Label, + iconLight = "Hollie", + iconDark = "Louisa", + labelText = "2F", + labelColor = Color.Red, + badgeColor = Color.Red, + ), + style = ServiceStyle.Modal, + modifier = Modifier.fillMaxWidth() + ) +} diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNoCode.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNoCode.kt new file mode 100644 index 00000000..78578322 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNoCode.kt @@ -0,0 +1,77 @@ +package com.twofasapp.designsystem.service + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.service.component.ServiceData +import com.twofasapp.designsystem.service.component.ServiceImage + +@Composable +fun ServiceNoCode( + name: String, + info: String? = null, + imageType: ServiceImageType, + iconLight: String, + iconDark: String, + labelText: String?, + labelColor: Color, + imageSize: Dp = 36.dp, + containerColor: Color = TwTheme.color.background, + modifier: Modifier = Modifier, + endContent: @Composable () -> Unit = {}, +) { + Row( + modifier = modifier + .height(64.dp) + .background(containerColor), + verticalAlignment = Alignment.CenterVertically + ) { + ServiceImage( + type = imageType, + iconLight = iconLight, + iconDark = iconDark, + labelText = labelText, + labelColor = labelColor, + size = imageSize, + ) + + Spacer(Modifier.width(16.dp)) + + ServiceData( + name = name, + info = info, + modifier = Modifier.weight(1f) + ) + + Spacer(Modifier.width(16.dp)) + + endContent() + } +} + +@Preview +@Composable +private fun Preview() { + ServiceNoCode( + name = "Service Name", + info = "Additional Info", + imageType = ServiceImageType.Label, + iconLight = "Hollie", + iconDark = "Louisa", + labelText = "2F", + labelColor = Color.Red, + modifier = Modifier.fillMaxWidth(), + endContent = {} + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNormal.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNormal.kt new file mode 100644 index 00000000..3b38b132 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceNormal.kt @@ -0,0 +1,104 @@ +package com.twofasapp.designsystem.service + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.service.component.ServiceBadge +import com.twofasapp.designsystem.service.component.ServiceData +import com.twofasapp.designsystem.service.component.ServiceImage +import com.twofasapp.designsystem.service.component.ServiceTimer + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun ServiceNormal( + state: ServiceState, + containerColor: Color = TwTheme.color.background, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onLongClick: () -> Unit = {}, +) { + Row( + modifier = modifier + .fillMaxWidth() + .height(128.dp) + .clip(TwTheme.shape.roundedDefault) + .background(containerColor) + .combinedClickable( + onClick = { onClick() }, + onLongClick = { onLongClick() }, + ) + .border(1.dp, TwTheme.color.surfaceVariant, TwTheme.shape.roundedDefault), + + verticalAlignment = Alignment.CenterVertically + ) { + ServiceBadge(color = state.badgeColor) + + Spacer(Modifier.width(16.dp)) + + ServiceImage( + type = state.imageType, + iconLight = state.iconLight, + iconDark = state.iconDark, + labelText = state.labelText, + labelColor = state.labelColor + ) + + Spacer(Modifier.width(16.dp)) + + ServiceData( + name = state.name, + info = state.info, + code = state.code, + nextCode = state.code, + modifier = Modifier.weight(1f) + ) + + Spacer(Modifier.width(16.dp)) + + ServiceTimer( + timer = state.timer, + progress = state.progress + ) + + Spacer(Modifier.width(12.dp)) + } +} + + +@Preview +@Composable +private fun Preview() { + Service( + state = ServiceState( + name = "Service Name", + info = "Additional Info", + code = "123456", + nextCode = "456789", + timer = 15, + progress = .5f, + imageType = ServiceImageType.Label, + iconLight = "Hollie", + iconDark = "Louisa", + labelText = "2F", + labelColor = Color.Red, + badgeColor = Color.Red, + ), + style = ServiceStyle.Normal, + modifier = Modifier.fillMaxWidth() + ) +} diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceState.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceState.kt new file mode 100644 index 00000000..232817fc --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceState.kt @@ -0,0 +1,18 @@ +package com.twofasapp.designsystem.service + +import androidx.compose.ui.graphics.Color + +data class ServiceState( + val name: String, + val info: String? = null, + val code: String = "", + val nextCode: String = "", + val timer: Int = 0, + val progress: Float = 0f, + val imageType: ServiceImageType, + val iconLight: String, + val iconDark: String, + val labelText: String?, + val labelColor: Color, + val badgeColor: Color = Color.Unspecified, +) \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceStyle.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceStyle.kt new file mode 100644 index 00000000..2a9624a3 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/ServiceStyle.kt @@ -0,0 +1,7 @@ +package com.twofasapp.designsystem.service + +enum class ServiceStyle { + Normal, + Modal, + Compact, +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceBadge.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceBadge.kt new file mode 100644 index 00000000..ce3800e6 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceBadge.kt @@ -0,0 +1,20 @@ +package com.twofasapp.designsystem.service.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun ServiceBadge(color: Color) { + Box( + Modifier + .fillMaxHeight() + .width(5.dp) + .background(color) + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceData.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceData.kt new file mode 100644 index 00000000..abc162d8 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceData.kt @@ -0,0 +1,105 @@ +package com.twofasapp.designsystem.service.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme + +@Composable +fun ServiceData( + name: String, + info: String?, + code: String? = null, + nextCode: String? = null, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + ServiceName(name) + ServiceInfo(info) + + if (code != null && nextCode != null) { + ServiceCode(code = code, nextCode = nextCode) + } + } +} + +@Composable +fun ServiceName( + text: String, + style: TextStyle = TwTheme.typo.body3, + modifier: Modifier = Modifier +) { + Text( + text = text, + style = style, + color = TwTheme.color.onSurfacePrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} + +@Composable +fun ServiceInfo( + text: String?, + style: TextStyle = TwTheme.typo.body3, + modifier: Modifier = Modifier +) { + if (text.isNullOrEmpty().not()) { + Text( + text = text!!, + style = style, + color = TwTheme.color.onSurfaceSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) + } else { + Spacer(Modifier.width(8.dp)) + } +} + +@Composable +fun ServiceCode( + code: String, + nextCode: String, + modifier: Modifier = Modifier, +) { + Text( + text = code.formatCode(), + style = TwTheme.typo.h1, + color = TwTheme.color.onSurfacePrimary, + maxLines = 1, + overflow = TextOverflow.Visible, + modifier = modifier, + ) +} + +private fun String.formatCode(): String { + if (isEmpty()) return "" + + return when (this.length) { + 6 -> "${take(3)} ${takeLast(3)}" + 7 -> "${take(4)} ${takeLast(3)}" + 8 -> "${take(4)} ${takeLast(4)}" + else -> this + } +} + +@Preview +@Composable +private fun Preview() { + ServiceData( + name = "Service Name", + info = "test@mail.com", + modifier = Modifier.fillMaxWidth() + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceImage.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceImage.kt new file mode 100644 index 00000000..ab47e2f9 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceImage.kt @@ -0,0 +1,72 @@ +package com.twofasapp.designsystem.service.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.ktx.assetAsBitmap +import com.twofasapp.designsystem.service.ServiceImageType + + +@Composable +fun ServiceImage( + type: ServiceImageType, + iconLight: String, + iconDark: String, + labelText: String?, + labelColor: Color, + size: Dp = 36.dp, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + when (type) { + ServiceImageType.Icon -> { + Image( + bitmap = assetAsBitmap(if (TwTheme.isDark) iconDark else iconLight), + contentDescription = null, + modifier = Modifier.size(size) + ) + } + + ServiceImageType.Label -> { + Box( + modifier = Modifier + .size(size) + .clip(TwTheme.shape.circle) + .background(labelColor), + contentAlignment = Alignment.Center + ) { + Text( + text = labelText.orEmpty(), + style = TwTheme.typo.body2, + color = Color.White, + modifier = Modifier.offset(x = (-0.8).dp) + ) + } + } + } + } +} + +@Preview +@Composable +private fun Preview() { + ServiceImage( + type = ServiceImageType.Label, + iconLight = "", + iconDark = "", + labelText = "2F", + labelColor = Color.Red + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceTimer.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceTimer.kt new file mode 100644 index 00000000..bb696cdd --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/service/component/ServiceTimer.kt @@ -0,0 +1,29 @@ +package com.twofasapp.designsystem.service.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme + +@Composable +fun ServiceTimer( + timer: Int, + progress: Float, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + CircularProgressIndicator( + progress = progress, + color = TwTheme.color.onSurfacePrimary, + strokeWidth = 2.dp, + modifier = Modifier.size(32.dp), + ) + + Text(text = timer.toString(), style = TwTheme.typo.caption, color = TwTheme.color.onSurfacePrimary) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDescription.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDescription.kt new file mode 100644 index 00000000..9635dbea --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDescription.kt @@ -0,0 +1,34 @@ +package com.twofasapp.designsystem.settings + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.locale.TwLocale + +@Composable +fun SettingsDescription(text: String) { + Text( + text = text, + style = TwTheme.typo.body3, + color = TwTheme.color.onSurfaceSecondary, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 72.dp, + top = 16.dp, + end = 16.dp, + bottom = 16.dp, + ) + ) +} + +@Preview +@Composable +private fun Preview() { + SettingsDescription(text = TwLocale.strings.placeholderLong) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDivider.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDivider.kt new file mode 100644 index 00000000..3d7af49c --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsDivider.kt @@ -0,0 +1,22 @@ +package com.twofasapp.designsystem.settings + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Divider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.twofasapp.designsystem.TwTheme + +@Composable +fun SettingsDivider() { + Divider( + color = TwTheme.color.divider, + modifier = Modifier.fillMaxWidth() + ) +} + +@Preview +@Composable +private fun Preview() { + SettingsDivider() +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsHeader.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsHeader.kt new file mode 100644 index 00000000..9451e4c5 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsHeader.kt @@ -0,0 +1,33 @@ +package com.twofasapp.designsystem.settings + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwTheme + +@Composable +fun SettingsHeader(title: String) { + Text( + text = title, + style = TwTheme.typo.body2, + color = TwTheme.color.primaryDark, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 72.dp, + top = 16.dp, + end = 16.dp, + bottom = 8.dp, + ) + ) +} + +@Preview +@Composable +private fun Preview() { + SettingsHeader(title = "Header") +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsLink.kt b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsLink.kt new file mode 100644 index 00000000..6d0a1a74 --- /dev/null +++ b/core/designsystem/src/main/java/com/twofasapp/designsystem/settings/SettingsLink.kt @@ -0,0 +1,69 @@ +package com.twofasapp.designsystem.settings + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.locale.TwLocale + +@Composable +fun SettingsLink( + title: String, + icon: Painter? = null, + image: Painter? = null, + textColor: Color = TwTheme.color.onSurfacePrimary, + modifier: Modifier = Modifier, + showEmptySpaceWhenIconMissing: Boolean = true, + onClick: (() -> Unit)? = null, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick != null) { onClick?.invoke() } + .height(56.dp) + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Icon(painter = icon, contentDescription = null, modifier = Modifier.size(24.dp), tint = TwTheme.color.primary) + } else if (image != null) + Image(painter = image, contentDescription = null, modifier = Modifier.size(24.dp)) + else if (showEmptySpaceWhenIconMissing) { + Spacer(modifier = Modifier.size(24.dp)) + } + + Spacer(modifier = Modifier.size(24.dp)) + + Text( + text = title, + style = TwTheme.typo.body1, + color = textColor, + modifier = Modifier.weight(1f) + ) + } + +} + +@Preview +@Composable +private fun Preview() { + SettingsLink( + title = TwLocale.strings.placeholder, + icon = TwIcons.Placeholder, + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/res/drawable/ic_add.xml b/core/designsystem/src/main/res/drawable/ic_add.xml new file mode 100644 index 00000000..89633bb1 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_add.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_cloud_upload.xml b/core/designsystem/src/main/res/drawable/ic_cloud_upload.xml new file mode 100644 index 00000000..83caeb10 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_cloud_upload.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_copy.xml b/core/designsystem/src/main/res/drawable/ic_copy.xml new file mode 100644 index 00000000..bac0f600 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_delete.xml b/core/designsystem/src/main/res/drawable/ic_delete.xml new file mode 100644 index 00000000..62d04a27 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_drag_handle.xml b/core/designsystem/src/main/res/drawable/ic_drag_handle.xml new file mode 100644 index 00000000..3937daa4 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_drag_handle.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_edit.xml b/core/designsystem/src/main/res/drawable/ic_edit.xml new file mode 100644 index 00000000..a726c91a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_extension.xml b/core/designsystem/src/main/res/drawable/ic_extension.xml new file mode 100644 index 00000000..e022fbec --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_extension.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_external_link.xml b/core/designsystem/src/main/res/drawable/ic_external_link.xml new file mode 100644 index 00000000..9341558f --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_external_link.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_eye.xml b/core/designsystem/src/main/res/drawable/ic_eye.xml new file mode 100644 index 00000000..02cc2244 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_eye.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_favorite.xml b/core/designsystem/src/main/res/drawable/ic_favorite.xml new file mode 100644 index 00000000..23d6089f --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_favorite.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_file_upload.xml b/core/designsystem/src/main/res/drawable/ic_file_upload.xml new file mode 100644 index 00000000..35253ee0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_file_upload.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_home.xml b/core/designsystem/src/main/res/drawable/ic_home.xml new file mode 100644 index 00000000..3cdad40e --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_home.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_info.xml b/core/designsystem/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..dca49aee --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_info.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_licenses.xml b/core/designsystem/src/main/res/drawable/ic_licenses.xml new file mode 100644 index 00000000..ccaf58ed --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_licenses.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_lock.xml b/core/designsystem/src/main/res/drawable/ic_lock.xml new file mode 100644 index 00000000..01577354 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_lock_open.xml b/core/designsystem/src/main/res/drawable/ic_lock_open.xml new file mode 100644 index 00000000..ca08fd94 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_lock_open.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_more.xml b/core/designsystem/src/main/res/drawable/ic_more.xml new file mode 100644 index 00000000..39fbab5f --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_more.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_notification.xml b/core/designsystem/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000..605362e1 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_placeholder.xml b/core/designsystem/src/main/res/drawable/ic_placeholder.xml new file mode 100644 index 00000000..dccdfd87 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_placeholder.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_qr.xml b/core/designsystem/src/main/res/drawable/ic_qr.xml new file mode 100644 index 00000000..5461a428 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_qr.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_security.xml b/core/designsystem/src/main/res/drawable/ic_security.xml new file mode 100644 index 00000000..adde47d9 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_security.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_settings.xml b/core/designsystem/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..c624d197 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_share.xml b/core/designsystem/src/main/res/drawable/ic_share.xml new file mode 100644 index 00000000..43178a39 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_share.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_support.xml b/core/designsystem/src/main/res/drawable/ic_support.xml new file mode 100644 index 00000000..1088db6f --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_support.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_terms.xml b/core/designsystem/src/main/res/drawable/ic_terms.xml new file mode 100644 index 00000000..655fca86 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_terms.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_write.xml b/core/designsystem/src/main/res/drawable/ic_write.xml new file mode 100644 index 00000000..1f5e69e4 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_write.xml @@ -0,0 +1,5 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/logo_2fas.xml b/core/designsystem/src/main/res/drawable/logo_2fas.xml new file mode 100644 index 00000000..b986d04a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/logo_2fas.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/logo_aegis.webp b/core/designsystem/src/main/res/drawable/logo_aegis.webp new file mode 100644 index 0000000000000000000000000000000000000000..fc9e2750d8fb2e2acbb4ecfe19d99661a201009e GIT binary patch literal 8180 zcmVPXMM6+kP&iEB9{>O^z`!dI35RXlND}0Db5ET93*NoAf{6Z4 z0DtN))zJD%%|QRaU#LCNUpk=uQSEM8(BB%`AJks7p!HVG%+(Y6lf4qO-l&<~Es#9m zj|MCWS}*E2c6apzNrJvg9VCeaEos0P1$*_REeR~329f{>P6A(TQrJk?SK?8%Ngi+q zfh7x!s**$^Ah9|VWKMPhn37~CEMV8Rt*V(oTu&T_{;SHTCpdAt_sK{FaBJIERZZOC z;0|XH(SIeBKuF~JC2-tGk|M=1!wX+yz`o7D_J3E&{c1skIfpe>E-7Z~dU?eMUHi7^B`Wynp8 zUshjW0{}4l16zxTln^0vmn4*w7C8rA2oRH)jO)H80D!R|09}CQDnWLY5x``B2!b7e z7$gzvb{yLvo$Y0I7V7{2KG*@!225r(FaxlQ8N6h`0YmTsfG+@7zy`=JK=1;9YXG3l zgc|_hg$yQi0W7|)ZCe{@wyo#=Bsn7zdj6Tk%v&9Hd*AQ-{Rrp;n7J1-S7+vf=tok_ z%#@iCwxsWAEo|v~J{daq{-)yRXieRYV)kU)wrOqKw$(AQu#d~O6LIC_&XS#mJ8a(} z+YDaT=|d-uwbvY$wrx8mNjjhRjmXHVtm?6C+qP|6JKM9{zSP>GOZ8Ih@?w5ebF_U}S~+R41y z9nP`7^O6-0RK2b^s=2Hvb)H4#dRr>p8mo&smghKQ>tl=KKJhN*)$^UMw&-76o!Y}k z&3$Ec9#oFIL%>zk{CWvMGxDj=`~36}4- zI#w4tklLxd$~UjSv0km`c*>)WEa>-s9s|{wXQOV|rOfxWYo1$v6l`1(K!IK{Bz`Za z_S0=81?0{IU`=d9%@__wpy#_iMMI!A%=d;W;NO|py{k>lW2(%tH6VDNIuz z$m~RZ?H7*^-7~S6lIQ2={!p*)7Tw}MDRcdA|M*{qT?nA!-p(w{W+XT>>23EIMJ+^q zr|dTFLatw!KP?+ELH%5O+ufszpO`xcvIDvE(DMG{+CM_rFeS_DD|VXi8{J85r69QT zc!R#_sQ9U@bv*zo=Q=K!_KV z+g5}59Y<|U9CzOo#YLm9Iwa_KJqGZ3PAoJ%yRC*FzPI+*`y@W9^%S##_xhBrq%|>T z|LK?-fBQeb)9*6AwGc_2lg*;GF1^-aJ+tT>^@-gYu(rc^%H?(K^LX;s1Un-(7(0Vobbf@??xyM}i(5i(otnbs(^_Xxh>^-%qQeK^>G16H=KN$(-h9 zpuOBXeSb&slG6vpzQ&P&Ql6H|w3)fN4HGQvsN{=BlZ)J#{{L_gx=Yf*K+J9Vi^J3j zu$@G6|3~imSPL;EdU8i40~`BS=B=zQa#IP+y9huo##XS%t$jK(=~5?ztn4w2wXKY) zaTjs>a&pwUkwftD%p{*o2hQD{Bu>lemKR++7Ti66OtY_fbxy8nX+n8g9y59qlc7Nd zh@C@I8SAl8Q;4PSz0_)XotGrNu$AJ1eb?}kYafohs{Y`us_uIrnjyQpri6}YES`>0 z36^$D6>%GipUm5RX8;xWs4kwwRSATg=NM_1o$lRI!|&U8<{AExeiZ}cUFvicarY?x zlYt4$JHFO0T# zT#^H7cdTFM@Y}&n1^Jg+($NM(ohm_aX8_T*G3#KE5E!Dnl6L73Y(6r;^Ew5{9RUc} z12<+xm<8j2@Fk(NU^z1Gg#SvK&plxlW%|#>YkCxG-iV-$@YU>PS)rzFE5#3zaO}}yr+|+ zWE+H^yUn@nK#)2NbBqWw@K8>NkqmP95w&O`3Pt~RvImi%UkAfn6NKAd$crEO=Jky- zT`p~cfO4rvrX&nj;7I5zL&zjf_|bCn6-xgaMjh26*$ilX?`wjgR^6< zCL#;rGJS-84cbtvl`RDT^xGF80xY)02Cju%#{_4#>|!Cv$Be(0I$+xB2hq{Yk1Rkz z_xs5t7-`LlozH*jP_kFxJVlG@CaR3Pi(#HnO%8c;oa;bpi}~CcQ)8&9h*1p+AWi;1 zz4jnlLSX|Kg%M&}F=d`bTC1sj)sJ7P-r@~}Sa4_rH%oDUN=?kZq3p+Q7#c z5oF+@Ny{Apj;uUIC2&KSNU*eYDzl6xQ5><%OyJzoEx>j})s(VdUaBF1iF4zEO43oz|>Yg z#)|u3wZ_t+x5N5_0iythCO4~~(iTZGWK%%^{BlNNr;i4!9L{VUJ?#Moc6tVXP#~0T`%ix3rigu|{nI3P zB8nHs#aTOtEXyXO8H8Iz?+&3@hP0|n>vr*CpJud=UTN3a`t|JfIHukm(E6~FV?H=Z~;F56e00j>NP6U8&DHTOiM-TTP^(x(6*Bxi(*h(_5r9QFJ*ON*R-kMGP*qXA9 zSYw8o)F=#PwrSBm!1fhuFG5;PXU$z51Y=L<@vQ@#Z7Fbe?I){k9UN1rLqJ@=64G+y z51|^^C`}Y9olAC~`l4i(Ej7Tk9qbvTZ4B#SnnZ7i-+p?ja1h@1FLPRNx;U1Q3O`_ghnMD$_rKvG8J2`?{N-w{8Of zts7A^JQO$_CYPcxr2Xx8E?pT&d3kqzEM`+P3%>eQan}t(p;3lq-M9@L<*aR+aX>6P zOj!FE9{6hKf2j4nn`66d7ub)P*5*#hR;OU33zB)W^srwgd^;iSL&!&yv5 zCOLXK8%6A`4um$I+2x5V!`6NT7fkB<6Hk ztqtes_TF-c@t*B@Se-nakTm`qQax}`8hY;CbWJQ{hOKJA(q;p2j3EhE<5Oev)L&j- z6gK!@zMvRXf1_6e7t(t1?a3ioOf}p*bi4=P8_n#|J$tN1G3w5h4FM$sqY+2+9X==^ z5O)ROHh}bZSFfRz&*szu>m~!zXk&z788D{ALb@jko9d~yUpfbF0Us<5TX8GG*)GQ* zbfR{y7LeRvX2*j%wy2Szpb!`LWRQX(`tHGhJ%+WW5gvk~CChVPO=UF=kCl*wS7%OT@<1pWz760M!eth9(ZSudc^Iv9v5kg}tpSkMoMD>{7uqd9xg%q_ za$oxzS`kO~#Y}8@ogbFw0Ys9R^@W*tiq7{~7G#5XCTd-o$)%Q$MNU}n)>h(W8X z0b2~m!}0$68_Cg0*uCq@Mk)!>J)D^CK}30rMJ2FpWEG)> zjlv563C2--n;d@K{oBv2sU&4(D<9_joL7WRmW6bG$8asEBp<}v&9KOyu8K*YNTYEwpoVkmm^{W~ISYsNBSs7=S#+t!%TwL=jl-i@Yd$0#)JKKEw z&DR7ka{H%iJj4HKcb~r9oh*6Dj&)PK%2n81jNu$48UQwND-}R*ZfhL2*O_aFV((WWK}(Vg`4L>nV3C(x#zp8qFqwN`n?HA^jlE*UT(i zanLn5+BzIv+TQn+W!unE#{lk(;MTgnSQ=|ls#b3j*^kT-uui)kK23;3Fyn*O%>e{3 z4L*o7)Bk@2+O}mIR^ljzB?h1;f*G3WaOhR1`$ai2U%a}E2N6QO)>mH2 zY>u_4B%1?+1k;#aP4u!2g2NU|r{?1mhKY`pL54?U61 za;t!b(rfBB@wGD>eA8+iY(w_gW&kkjB|`0U)AG5p^jwokesjArU*qPp=Q4QZGiEzG z9n0Jqe4#hC9?asVfD$|$`bQQM^b8tCu=22T;-nkgjDI%PR!*gcWRYMTwdE5*E&byf zcWzzx`nb9OVdYX@*c^RzI6W!@e8`kt^p=;%imf(Jxvr{!Z4&?qn-&5_`8$uW1aEXc zu>IL3(p$zd>Qik-3S9bCN5u);4CT95w^mkaNFZSa|121N(`0&~&D+NZTE&}QD zx;mPBKDzqB6?d(hUw`^*k6CSt?kgv}4rxU+WMAB_;Om&Sv|3&|mQ52%3t+92`r+DG zM}T|mRJ!fAD4IQv)^>dT5H;n!$tx9IB2ni7i#MOsWnnuttM^Gm{MNHqx(Bc zE^Du6fO80lu?#|mxvQ#^d?N$GMnPv5RTc}V3C()1`=ig@Y2BLaRrWf8Jl|verKz&ppDVI2<~uOtN?{dnT)OQx78R0uU%oYR&Zd zd5@Mvn!#kVvD1>d?t&D@C{&JRj;Uue|dvS}vsmSJmFF*D0nHy!TPd{FJlEYDE;45EI zQ<+d}rd(So@U6*X3J0~8slnI2mQ3qbTSHA=hFBbzy_MYRqkTk zZ+Dk8ck|7!J$w8DmurYLP^ivvP$45LbXPUEns2OwxBllTv9N@P(){j|cqOO~51|@+ zXHwj1EMo>7Rk)hxrni3m^=et`=}<$Q8E!W1KU%I=`h5)mDwdt`FvmopC@_vn?(R-j zL)AFQmN!yq6tX{>)M$f zTZmXB+`AK6B9=iYWUKx2?Ul$Nx|^}-MbfVb-7B*ftIVvAj@y5^5m(OV!#Fu?sHDZz zs#SfZD9VrwUkH16Vd7Zk?phRvvT;N?(E1CJL(CBS?yKa8EU{IyD&Ub54vzUy%%oCp z_ca^=Ee;Fztxx{!8JZW%`eGra;N!U8@ApDp2Z)%xWGLlN&okM`60D3I={mAjw(3q^ zz2%6!L-;Ib$si~CMq08soBC{6NdY-9U(8D%w_Z+W!B@4#lRL7`JU##F8EhN3Njsyl z*130ukPwu&>b>?Y-+JSethG%CKop!kYJPI6Y_*afJ!rGT@+&Sq_jPyQ_!@**T|Z6f zAWSkS{lIYA@7T4P)+t5Lm661PEIZlpo(6!bTERS}4(xGv*X|js-BT`|3zCB#Xl}MI zzl=SUoRT^i{K9cb9}YkSsie4UzyJIiG)Sun00#Z+dBkkv^v z<#a+bWEEC-RK?7y{wgvv;KYZKP9M;K3sl8K?)PFW^J1YX9vXFWFatr$6f%SFS1er_ z3verm2{D=Yb(=dLt847OgTu{D!~R42ZXq(Z5kKDa&CS(D%}xI(-IDBz4i z6^rr8VnXJ*4%tm>%VNR)Q{!@@Xx<=?mhK+zT~Kl z?k@emL+oZ|uLlq_V-Ez=C539K>_a36GmtjP%ZPGTB+E+rz#nh#$)(y%*O$I(uiOX$ zcWu_A;7EnOCcEpJTxxP1M2MLtpWQt%B`f-%b5-PYMl*`i9HErper6RmFWB{k*OA3Y zm)Fr@sAjgIwH;Cq9E6~{BrAP+R*hW9<1bQ7Vmb(TRZH@u)8o^5SlQ8pwdvbqu3E|6 z6;D!@ihOMb)KG&NchXEI>W3L+ML=(~Ug%=2@K%8jupFNF@i!vXie*I<0*U#$uQKC^ zuhD36kSeSz3MyPocu)Doxj|WxDJF31upnoJie+WUnJvOp%3^LtLO~(oeC;F#RiD96 z`5yh&8#epR)0w<_^Iu6oX7Nux?KUbiTv5we)a5$MRT_|7?XneJxR9T@Ej=3pR;X0-?dWwF4W>G-&^ zf@H>&MvIn=L(HbqRV8_@vuLuSTdruQ=raD5nx)*S<;w&xE`-*{;&KO2`ldtKYMNc+ z=f3pb-1(tD97}((ec;;Wfh*g+E<|QSR1Aws2a-jvm1b{Gg!JO#VRwA!$*a@#mVyvf zSw_SP({iC$XWm|Z++}hQf@RcDt|hqj&i_3sCl4{s=Al9aUoB6GMvwy6xc%N`8;$GQ zD>s}Dk8sq*NI9z-7TF$JY#waXwQRZSsr68f-qZ(Nc9e`}1hd(J=;-RoHd)b4Cj?ad z3>RA2iwS=(PFrO!4r&67z{3xVO2L_%EB_@v*~o z6Ud6UrEjrBO9(+G2b(R7?gv>t8wUM4ge=#gDfWFDIVk}~NOy({wK=Pa@GoC@C#rDs ze<%I39zLpw%TPP=JpY=}_6pE47Ko)g8c|eee6wvb=yA0*N+1EfQK5=d2ORx45-p3q zli{j$jJ4CyDNVChin5t;neRmQxWt{OuVk&%1WTioU{=(o_~>%2fUNyryvz>OoG~Mo zv#HrKWcu+asGK?M@AI+6(28)D)#`p_UvF zat*a@9huYlc6EYx^3>X^XTwCF@m4v<^0^mXo~G(;ICM^zF>w^sxCAhlO&Owa350Yx z5r>6cRu6(kf$LUIZKhaev3$#dXpmx((Ls@Fw&^{Plj6spd2V91?#c-%1EGvVuJQRB zOFc7DK-1(Dec?bzzY&4DpC$pC*#r!1(otbxp{Kp_A*eu`ReDws0WkdX~n)AR%I=#l_a zACewg$v|ZvK!@b~jtr&FWnSQ{CMbg}h@)tY&dNAt-q@+i=$=sh4|qC_RR_eXV}&2Q z(8oD4URMFmiUJ)lVD!l#klgAxNa}zHH~^t!tc(i4%+MO-;Dw%lf{XhAM=UJK;~V6= z4p6q_fi)F^p^R=2%n%zbhM<%(Mh(cz&nDX(VY z2G827fRvK73dI53IbtJwNXiF!oGe08-}W5to5kMyBF5UWQ^_o`C$1!f;;00aAeF=1 zp4QZm9M}3WnGi9KJ>P{i&oZDRogxr(=Ho6J+aXEuYmvS@tS0JbzJZ5LGZ* zNOQ%uHr2o>L72H(qeU&%q6ArnZb%G|LMk930m>9CB-sF}v1%qJWnpI=^jyG7ltaob z3#_eGNA{zhAo+^c9~jjs9l%2Vxky411qGgOAY%L_)BxB=wt;-Hrw$B4PNjjwP5VDz z>pgoc(sz-@keoOP zc%Cz6E~C`eP&1U;p4G$S}K=|LD^Q@ezN2M2E0z>&{`s zRwfEK`nwyfnt={;_+oCAfrP%+-#{XsnW`mif8^B|P#6;=(}5B*PMBUk+_U&Xw*clh94Y~sf|AtuGsJ;3m^gK?@D?=p zzXS)=&<_xS4X0|hkw}_^jj{bx&NhC;*Soj6-ZC)?l|JL aBOV6uPRpDP{aaH2e{5_1u;gR@pHb%U58b-} literal 0 HcmV?d00001 diff --git a/core/designsystem/src/main/res/drawable/logo_google_authenticator.xml b/core/designsystem/src/main/res/drawable/logo_google_authenticator.xml new file mode 100644 index 00000000..8422962b --- /dev/null +++ b/core/designsystem/src/main/res/drawable/logo_google_authenticator.xml @@ -0,0 +1,34 @@ + + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/logo_raivo.webp b/core/designsystem/src/main/res/drawable/logo_raivo.webp new file mode 100644 index 0000000000000000000000000000000000000000..e94d4ff1975ffde769b4788782e8e2fae65c8143 GIT binary patch literal 18048 zcmV(-K-|AlNk&F^MgRa;MM6+kP&iC$MgRaWD*;9j35Sg&Igxs&;mwKs1Lhn=5Yhh$ zU^xLdE+qgjp#Rh>btWSRx!G|h1MF1U1(>;~TMM$BO}@p2^pB?)0KVW}zyxcsNytA) zFk#r*?b`ziszOy!RW*P>n^AH~E^X17@AumSIO*mBIO!_Xh|NjP0vQS%2k-+jRSVz5 zwBQxYsF6(7kZMJ>0mXeHA4yILlF^kxdb``G)O{@@<-tt!-IFE#@5mauAv@5e>_Ojw z{-?4Hb(F~spuIfiEl+`7TngE0V-zrB1gpd{?4G;h<1?8kcNCUa51f*du z3rK^c94TNY8+|+qs9u#kf?*Z%g8+bJvg!Oh0nrrj+e@NS!93Nl1OP9u6|S6S2tZ(J z8mLqhJ}W+|RFjox#!ms=hzS7bD#bwW?94F3kVg}G4t!4hBRK z)>BD}0#p13_)0$q5c-iN(0_z|Kmfe<9b`NMKGU`O(yaI5do9=+*@{f#DlI3ksPSF0<&w)mD zFrWj_XziH6e-`=ykVq??;WN`9o(5G30cntd)B%?OAV{Ek(jaR1gG^ydqjjJS2W=xs zQp_LrJUe|yAR;EfzgEA{cA%y6($@`r^)Z?}(d?nAe#FnTnhL71YQTkja?Ey9y0DA# zcC%G(yVN~|5DO39+GZgX!ncl&Ax<6_-F1|r&H3QAqbgm~QIoDKkLYq{yV-i;K_1XU zexrP#$6V2KE=+GsM}?-{jubug1zOs6V zua=mn1LKg~YM4MjJPsS14`4IMsjY2#s{JyQDdT_&${0w6gI`ZpY+&JUuYg~`rNziR zAGx({%WbnCa)qPqXo_Z=4WJ41gMQou8&F7vUd|CJ$AKJ4k|f2ms%Op6fY*%@=z;!% zu+v?#Als^|2u#g(p97rBA-qD=`PH@k|0Fk!_W8AB_H9__+1Imq|1TwH&%S~;AbV`9 ze%B>cSC?8#`|x8rWkb|X{)}dr7&i)yJ(voH!X0Hiv2lV~+WyC_IZd}{C zKi_{zG7P1$!l=R4fW9sbhQ9uTnMa-He7p(2igCoJqXsjN3Zn+w8R(FM)!?=gTb5M6 zuLW$|lsk={@0*zuBxng5iUsOIskPX=65?*e&dLAJ ze6na;satFB^wwfdN)R6y)As%X@h~A?IS^&N^{-4`zHmfc8uXHsg_>R;#kMex(v54>{Rz8rlWr=UKU7?2#I5Rd@5XxDMxKoUqX=TV>gsUHJ3 z;{o?{C7~}TIF1^m7|^~)0TR!&8}=$r_R;kZU41>%rEnU3<((&UGP*8iAsY}#ujA5K z^$f{=UOqp5<UP4`4#`_j@)Tm0F&T2}8>@FzdHi7Zf)7_tty;H+ za!D9ihB^D@bi2A*^LZTiPmw~9Oa82K$IVvY2-WOC=Ij6a;-w+C$E89Z5Fiu{h2A7g zTZK=}K;x-##XRIwcA)+;SP3}4nF5RVectJW+NYsk<(~!3|!s%Tgh0Pycr8X zL=;Z*V4Yh5P3=(P8yD*vcaQPN8kTG zaM=Bmm`8JKDDZ>lH#dI@inH-OzJX0FzQO$AOnq-$nvWv0odKt{bPqHo(eKledKGno z#3(8-M5cV+Ahk7ST7D*?Mh+Q3R61d&+i5VJmg zgnM59wkOLh^9M63fhpCAJJ#37uPqDQ6XTqZL+-^iZn_YK2lWKOw_}*yW$%!ODFz{? z&W+qXuk+$Eol~87SnCWL$FbVVT-N-XpT2*4;!82EGt8Zc7%5&?E`;!4Jlz!Mbq=!E z5JFr8mWPUcO#1v#?tL@PcYo*cwb?Sx`yOToO9RoDwqKn5beFqH`E1NBq(>eSf!*)< zY}mO&QpSeT4&87+6raX;a%9L&4ifzC7g_%Ybo;~En;dKfHnGeI@ZqmHp54`BfUmw$ zCvI}}u_SlLg(;jup5>^)@%6%J5-?z1mm;3~q4@qHhCCzWlg^<3Got_HaU<_g+>(QAkD91wS)=K{&J^xA?HUE zRF5&#uj2aq^V=IQf};Q9^^L&nV3R;P+$Gy}-nSmVbV^0u5AKMmxqWT}$UX(C3mXzm z1dt3^Tb(2*xO!b75rp3^a+f3E;Sv2P*ro4WzTLrA4r{^JRI_98`n1;9y`3?tEk}Jv z;-a_!$B?yHfCGYqs*8JngbFm^6+`5SkcrXw+m!VXMO>7qo&s1Ku7|6lo&zmI_w7zi-|G2|xw;B7*l7=?Nrbm-f6$Bxxqfu+GZm4`WjyC%rh*SMNBY8F?9 zEK9j;bdgDm+&A=cfdi6Y;h3~tf@{FME@N2xE$v2CZWa(F#>XUBZPOEEW#aj2uzM3U zsTbJuJicDwL+z1kxie}Q-hBFrQv;W$T#Iu-TpDX0L7QCQa0HrKDU8X+3q}GYWnE^5 zD|zO&AYvy!R3U~$e;wmsJ{d&T2YEHg+h_C5PGImMYa{E??DPJhbi2=CE6!D4tM1R* z2z?I8<=|4}q)}OH*gm4)ZbW^9k9{S_RoWzgww_61%2-$k*bgu(iSaQ)C{zc4Jzy2V z+gnHP?i|(&-1NfI?)Qhwt~kp2SC!>USRaJGHaecQi8GSA5cLJ%w3t-+&Fqmi<|u9M zY(*Ibgpqzj;#OFYBdfWedn*cvJ@7F^WFb??tpTtD*ZPRoU7@$uvM!ANM__ic2Fp!a z9sD?8fZM(%u3Rk_JM2lp$isF67=oNOh*#lEFI>vaLF$2K1|X{!>c__Y@8dmIlWL zw*giTTIVXagqzzugso5!7BP3aSXKY!kKLJ4dCcERK^4!pm zgc47I1Jc6W=dOWTAazVBGZ!txoBjA<5yKp@G)5t^kc8atAZf;`ksX=r+z9rLlp4@E zrq%!Z!*6t~?uv63F<9E)bNf4R!z}xD_ z!f7DnK+*@m0o1jlerjZX&;|w@wR(rYl>t`5dMm0dp~BLD?2`+vRQD8Q6ZBwlD*v9j z?ES~HvNMClsI7BN+88X33C1y`OodEKx-w@>Z7Xwid!E{jF zMNN)LBL_)nskeD8Bh4VXoSt+d3z9HX;K(8|09gpIq(;_?peN8&p(UKAAdS#%1no(6 zZ8f87V|dkbxw$I7e0f;u*po)D0s;W`wV=JcuDG!81 zHi9Nxh!46f$tcJeP!7g@j=GF2K88hvkN_YZZe$O7wk`-b+Tg{z+hjQTB^J0x1H#op z0fBu@@a!WT2H=R1h~eHn4Uh?W2Av#N33>?hgMn{?E+#3Z<$+is1wtaj21e?RifSN^ zgQHXSDac@*VD@`FoRuv<^R4XVbCTWrl3bZ~+-SNeFbHFY#Ldl))X?K039C;)LK2SX z&<{hqIFeZE2e7up7zTGjH;} zKYxoaoMVBd-PuEIr{4VJ-c>2zB6CShL5(~Gl7y1fnENhsJ%1tPlnEk1r3~#~*11cx zDz52b>s%O!a&$k|JHJ+W|Mk_o+7!1EqEQ5fArFBjYr^#qc*CS(UOo({3q3UM)xl0vnw|31~6 ze-aGAR`v*g@TZS;Ot{aE0=XaY5%8*NejQP{q4A25q6JeRnj~DQK_DMSmt6HC`?qkV zf7lc7d}%(_EJ}gV*#mV>YEK9)%yffw#|=)DbeL7uWm)2)b-=%hZ7^m|NGYZ%W2^ot z_Dg_o_5iHb3DyESlGNTN;rQrAKiLSkSOtCf1mRvkn}f=}9TYWy;cOfA{}% zDz~N&suM5tQxOn>ULh~EdIIm$!N{I z3eZVa6dFW_X5dt+ghVt(mZHOzoPX7OnWeDx|5vT)>7M`=r}?y@|CbB&6LT-+L{d@J z=BJ4ufFhanUNym!kjc?H=2CobUYaC60{7i|`^%5Ql%EC`r_rQup>Kwz?RivC6p6yQ z`jm0eaUFNs00ZIlVUkK$(#eGW+YgGTRGjD3NtWLk&zOK_ogwDHO zZ4j;L)ZeVx=ZHSO?*iHMkygYFiR}9t*N?})4{P|&`hXY)G>y^0cWcD>?X;l{{?h&NNV`3$iT&UMG%y4sEKYw;c8kteHy)|oC-tM$jPhmQ@os z1>!L_3^J$Bu%Uhro#NuH07YqZJIwUKK4fV69g`?ihI-UC-B`V?Dw5z56Eg!|00}mk zFS|Yl#QIEzJel?^zibf<5r6^BK5pmNwCN?50~BLF{&3XeF8ZJO#Q?K|+5ck{ey-zn zzf+9bj)?T%=%&6YyhKxq6co|x*bguiLWnT63rZlL&tqf6s=hDE5|SKk90Ux^-UTy> z6hIjrJ&jQcBnDCHK9Iqxuu&k5Ta5(hgu6e2nu^-`a(#XCb^pif{Ct<%qV)DEZl+YZ z>!2rDd?{c6Qd|eu6MR^rD4{HF*9v>=Vq?c?koBlb`r$K8{R*_5edDJTHnJN~or!q# z`KV!ArUS;*#5HvHYT1>$8YnX+>cqBjrGNw?CXY^+H9gDeHcl9n#yI5TRUftj{AIPf z8&iVxPPN{24NR~;8-})+YXYH+rY36XjVx4KCbpcP-BGK(*7M;9GawEA8msO#xAU)7)-LEXC)*snFcVP zZhT-Y6&oRiNI4+P&JM6NN!6QmgXvU6uaK;PZQzE0n2{ORVn*m#*p-SI2)V_Uyx3k% zI5tP4MGyT*x5~7;`YzW)NKC>MM%*6Z%N~7UIFBiz&8uLu9dzEw;ruA1h=SUUsQnH< z_W+=Q!$fZVUIUtngh}H60-w8pTpPX9?Vyu!3qy&1A|^MY_Eg~V{nAtk&SPaRzpQ(XIH7{>Y|_red1xg!O9Hia9o}NPVh~Hm5RIP$s}V) ziq+Cb0C)~vhYnvmH4Gig=3DOZ_`7`&lp zA~p01%p2yBWd#{uFcPB(IQuIhgEqPblloazwUT;zCE#s76KN#H;aG9h)Fps8MjlYs z_<=PXx!Y+PppA#BkMTB`n`kH^E`wE2y}HoK3-Gx^)#Q0Uuw0r|T(o5~7`m=e^NxF# zt3c8rXvJbyQR5;rm3a;BorZbLBD7Y5O_hb{MswbU4G;rj^)eWKr(6nPz#HUiHW}?t zk{xu&^zI38vzz8Vf}20WNLj2C&qRP1;anY~-}4?;U*A8#hR&82SHR0xM+s=g|*^ zyCY|i*4t(GmWoLSxWr&*HH)tLZlb~SX`t88TQRB%);+=^Q9*(tSn9i@4{0eF$<)ttU+Sl3RcmIni_`NCXLl zC;~ZdACW*a$eJNJ=36J$D?(fh@i7V=eaI%zN{|Gsm865F>*fO)_vqe(+w>sDmAR%v z-?yoGWsc*>=_4XEBVxaD0(?F1rc;|C>AjKN8d{TnE$H6YZGs!0-rjGb#ZN&8pWxHz1G+?ph}^V6W!3%~~@6NUIp~jF69Ipa#?j`qS}XB4mCHO{Xk! zJD~6EgLWxn!p9VOPARCv=`=+&l%C44DaeADWD*$=KgmYDBTxB9_h&>CbU z-J?;a<*^52Qp9X>Ni<15IR%Z1lN0!u2WsRooj5fp9m=f>ezin!lC&G z;PE$ky_zc%(>iVB*-H{J@RwLCeZD%@rSdW=kC}*1V$~u+N`MNJdc>r(v4Bs;F;C6 zT{0w#agn>~&yW@8FHirnQXsHW?yy$h`k8;8TGe%LjPe<>Fu88w2t~+4YLfl|_zS3A z3u}dyz_G2=D%JhqC2>437mN6|g4Thq*H@hf!AA80FDTp*SlgYBIA`<;`cycRt43_TbzQER>44ouED(yZ1lpKXZZlwr0rc0!2xb7%>H8d zMh5_EBlth5ftI2TXO1Buu_#F|C<-U;7^KqB6u})Q zNeeU>jeXC6Bopmk;6uU^AaCf)cfK+|U7g*!@%%Bt%@K4nc;y_Eho!CE|19r(`rlQo zju=YI9Fv3;xO257hU+K_q;6A>$AXK_y(An@Q0G7MundgL-sMCN8ffdfu8wt#O2I7s zVuEI{PJhGNi8GBMki2Qbj2OU7sZKm>;sn)LoW}Fv8=cs!m@k-*@8JnajPXQ8Dtws- zWuWANAnRETtF1wCUy!2(8MqWVFo_gP7+DfD zpeP;v`KP+V7W@ILXarY?gIK$eMm7zuOFf=E#DUwa&xK!9a5zTAdb}PLVhCL6fALf4 z*n;9gjz#y!N~Vo}$V7@u2%+15AY z;W&9<7~~KgFkS2oDwENqw@#ENxh*#G@k_sG6BwMEJqn;S!fz>o66?6ffX|JV^z`?C z)4!K;=6M&sCV&I!dNk%mA}(uzB*VCPw#N-NXBX*9aU(>!Jb3H8&2 zq|qn@o(KIpt%Va=#0>Yt*#u}Evjj1aJR)qgm1;0M(;jCK>Y9~we04r)ly_=FKA~b1yipSf$e_#OYz1Kf1 zpl^hR$bn-bJ11WHj>ZrtETz1)7xT8`Y$uwN6MF0zQGLvQ&V*bfFTytxq%4q5-AGv&ZNkVH|) zSQ1GL0Jp(@*bam)K()5?L}4pJH52ftUa!H>CYPP+th{jkH^0e=v=dRp=sL`v;OYSb za`S0xS&VPT?n&z-hIA|))J2!1!_UjX0n-O!+5rR%(EyfK@5mCkVOc~6Ef|#)wH>I4 z;T{<)*B-=6<$g(X$Ai@tNy)}0NKk-eo@XO%3jpLv@-gQ@O=y&at$1+2?iq7L@1+M& z(F!2cO0%p2J|P!D+i@_;+k!<*NO?Ncu>gLv8Djw7tbhot&2+|)n)>7)X#hRy9DgST zNK5?tZhsUgTij5)j)b7b2unLaV7W99EA|fNuDO|4L_3KT4rrp}d3%d_DaZZ@Sb4VH z_pSbH{*Iq_U`6#wo{X7*lP^?7k*wlQGu8ToxkAf zk^;2V6WkV1%1 zOec8;=ylozP8OH(Z2TVl<9tgJfJC(k;7DZknV1Fkog=>6 z`3K44B0^mcg8%=hItX*y5WJd){!PrIW9pO`BAt(cj`MF$el=ylE5~{(?M}EP*~FmO zzT0_+mG1`Klc7RPoLGG(F`)64`MIh>F%|bp@3pYV7!D1l9o)*>j*TWE{;vAE$doiI-&#eD!QZ zz8{m)+*=P`V)^HKoCnr#GdDDTnvY6j;ey_ei#X2xS8(|J{lh|wm+)uKkROmRkfmS^ zz~LB0Nav@=7kRl*CIgNG7y-YEaX_M~i^XR+E*m8IflMFBe1T{8bmd>Y^YEeyAH;nH z(d-yajyxy|HP0Kj?t?rb4@}>cC~M8W#XV4zfKOx!3m0Ta_apvijzb!Bd&>ph=>CN3 z%`b4`Vl3Q73`glnFg+~Smu}_H2ROq50Cg539EDjTq&l6d_Ly&0qzrIG~sg#SqaV zB7T5r`>)*T)E9e@=)0Xj8fhuvs~3!r&FbchB@luiD{!_SgiCYzENIOD6# zL-jObNJ8|EL$BMyhT&_E+e2vefe-g?xIhv+%LQ(>I7SR$AdvIto2}=&rICP@%laph z4A5c1&;yBN0n)47gFCJPuomt=?G&X?9BIDr2#kP4kFt14P8x6m>%v{Q7G7)bzcrdW z?X!iBAqjVZtiVyk(UX>ICJL|>v57IA3VGatM@$4|i6!deBO5rfX`A7YEnFZ2ZYU5t z(NV-GJqP^}@EgPB+^ht);@lVi!xU1_HiZPs>+yq?m z`Tk`te&4Vn@@Z;D4yG@l{(6j~c}j*8grxllQcTL{PAlr~KDRsgFBo=u{6C8i3Cxc5 zFM3wLF1rE9>8`(%=h4X^V02xhV-mH~guP5!lFU!`AYE21$W(^ymGoW^i*+H zc7^K|5&C#Mt_a*1my5|+4-lKsBI8gq8X-Pr$fv6wf)~IWBkTsPm)b>X6Jug%T6Q@# z)mH~u8kff91|Y}r4f~JT0SJrGVXx8QTAN5<0MG_QOrRGP9e`3MqR?CV9f6rfHU$`QQ+ED z#QR8yNg#wgG;}=($)Fu5k7)@p%D~iDdgvn#-kB0suDg zW@|k6@!5=g+V!IpKvKJ;$r^iQ?&&gbZ~<1LUJw-|;sCA0C<>B*E22)g6x!Wr-E4vA zO=6_)=$rf9-Mi3qxH&p%<4Y?n2|O`ut2X!KMAKn%*QYMl1>p$+i`~`X&ce=?-uwPI zvOr#B=~ET?<;xelk62jUm9{u@9?N4ANYdmJ?>x_(Bp|cF#U^SZb(pBDgtNi1zq4RC z^j*suheMP0gOeo_6o58gIEYtTttDX~RZn8}AucR{ar<+her1MjQQ<0P<#FdNW&2iY zlPI`~YVUbPx4BxcYym@Fp_ z-iFmur*=LU$S{aBTPNv(y4m!+f?ftP+g@nhI!0Wcvkm+jL_Szc8w3j7;BwL_Ug1Y9 z+#`Ub1}vifmXv8yuYIc-B6AqURYR`(7p`LzN83LT&#n!y0YWI__h1ibx}gm(7x4K7 z_{83(&pBQ|xW7{|nL!BhR(EgJLXk+3z&^FSf44AawU!nLI}&*S$(^l}{Bt9&kOU8~ zoL}{~sEBy3L(>*y(3K|;Uolf<=@#lD9gBj#m>t|8>)EwHD(_MREVUcLy=rV%0Z%yH zb!e=5Kz4nFBVV22g`r`k%2`ECu!PQH&X4FDv;_#em+qWQj;y-UCjo^0MDKv%g(n7K zT5$q(Z(aDe8VU+b0r_k^?STfwDEXArowvY`zWv!(@-z-nlv_70F|E0_%_^oRqt*japPs1mHX z=5-ZDag~tY+bd!CKYrtU?Y;;iGkR-#cuXDU`2a2Hye$q833UrfrZ}e z>pS3}yZ$!G-n|{!E!z6A%@k0a%4nHaeJJv%2ib^gsSUWJ<@s-Xej4f%=o(vYV~N0! z-NeiSKCGYY!j^*FXEo`xYvE5;bhS`+gW@y?8a6rKhzhYhSFG4c?o zUb9@f((caJZ1e{9^b}Q~5pfa_M2C72xMUFw^oVNzTlTil2_+4PBZPiI&(;FhI%~1~_QQv_{emQ|ki0bKk;=ThO|%U_ zmgOsK+8`0)bGsY7+@3!xY*I_`d}BcT(R(la^QFK4{I%e^#NtF{IOT}ggJS&^=w1RO z!DE3l`O3B{7Bt&{I50LQ92qF$%Suydv3r>l6QYE93^@IB#Dfqs?r%{9Cl`qDBvW0j z81PXRbjX7Q`d6Sp%-EQLgxrCYtYCC)KJadB3^|6NK^~JKgMu;!Mn@T9GKnCEW`}S= zol#sK+MBH6o84h~%nIm#fhN{lzp^FQwYlvdA6_^5)vG5VH>6n^E}`;>4;d0K(}kxq zQ?{5EK`zg&x+p--gE%&n4GAO%K4u}eISTz!W_g7!yPrCw`EsGS*%_UlKFzUy$;oD% zvba!2ip*`411t}Hjn_JmDj{fmy}1NQpoNWJ2l_oVY+?$R7Z7>bi(a!3{=@Z{_* zJROSF79O=)|q`P2#PKZ@lLjkd$>H>ydr<(6*L#Z{d~#z}8eh zmHUwqftc!Iu~@XV^aVx4*Vw^#CrDir10kfQO7}To6S+Sw2>L^6F@RyxHC$}A2-+`x z!G6!)KPtf5K(=0$)q1s_v)rV2|LJi-P!nj4S-{DfO*@Y%kGc+Yc@72G#QFHWZd$k2 z-D`V=-M&OmI;2^GrbWTa6wB149Rx`@2@REv?>x=gAI3>EEX+}Yaj&! z(PHeH0!jiI;-U<>8zZFAv;P+&7QT5a{ty0HMJ5n{nRa7aie!%@lejxJIo-78DzxHy znG}0BreUsQ2?UVEOvr5>Q?8eBLYu=i6im#n0opkZERltXRW8gPPGh5LWzNg@otumR z%IO-A+E(2yaG2U7ioOnwiV{f-h!!y!5M>Zz8kY^Qw?>0(k@Z&G|75i`kW4#^c7RyD ztn6d(lzF(e^7a2^r5E^B|s;|Ji7 zOwYuFK&lhx*5L92@+_upK`r%xDUOn4qj^S+qW8%CBrQ~9bS_{rT|Pa&-cNf1(!K;T zxTh_^J*3;+8RH2o$7IZT2|RYy%l|#k1F_9yA8-C&CU$3x!`wyJLDSral=Qd2LjAf= zg~S-eRcC)4X*@w+6CzfW9z<-p(xVco6}siz2KswqNujd6e;av=+R~;)m2m>=J;Hm;e1! z^C2I}?G?b+Ri4K@e$?b^?YxU657MZ0eT;Z4blC2`EkSU;ac>q$B zae1=w>mRZgw%tpW=SyR?PHR0VU`6LIy!%oGZC^v8Dn{4kwo{b-g_pQ~&79pcz7^j6 z%F`a)%j0xDIbVwE3boIjV#V%lS-dB<0@X=azdF0|@t9A`bGOTD|Ey!kfLs zmcLdb&wd{lG)ZO>?%qwI%5VjFTN)_t0(lX5MvpysEP{?ibqr8c3!yakJ+P_lZ8_w= z;pNtWY`cRjl43R0VpxE~@4fP`^%g6zI7$Y#=k5yJt_Ivj4%|C`rd5kE-&e?3n=&=S zvNJ+gbw796oh=KX2)aN!St!e88mok5 z42fot-hKJwieSF?gf5!tdyqg#thNyXN*&rC!3wZ6Nu!RY0XjL)W%x=7fdfvAF~A0} z_lNI&P(bu8My@_DCI)XW(xG2y#DJ9k4|j^%4i~RaMM$w8k9R%Eu&|Q0#kkdG@c4FJ zgMQ>Oh>l>!j8p#-NY{5x%8Qx3ei1b;hygq<%+3P|mV0KPfT1qaq0c@54=27Qh9Vxm zZXkp%B?xQN3fvN}3~v`4sLSP+ssL^nd6WeJHr~}*jIG~S;?4%3jmTHqE{>e% zgiSh-7+{^mrqcmIb;u5xGU0{&`9z171@O%5tL2c%**B-%Hc#oBz!X=fR^WOio;hAj z1!Irv=kYKw-G6VdN1tHc3*p z|6gK6S%JlSa&ccyGdP))*H_3``X)jgaz6K3o{8r#3!8W_LA6z@-t93$eHw8AEz|>2 zzt4Fqz$Tf&?R2QmBA#N29F_adcE!jooxl1YpBZ0ac^-CPT7N;3#l@nazy6R*I{{X% zyzo&^2x{qNG30|_3ElbiT{KGvYVJ(LpJ?Q#Xo`?$ zVhX*Vw?+eBz&eU@dFli2EskE0#c7-fe)lvAR_1AJjk2`C&9NBMNvXXAetYTi1vj}J z#-x!L;`{6a(uou zV2JC|T7_E?R@w+S37XP4;kRobiJZ_G4DWRVXs%o10bniM6W{r(A1?-4$iiocZ|TQ0 z9%CaxV!*T2@yf?9OawVAdeFd%0AW2V)mG<5N@Y(7h;<-?9Z?1n(kc`<*a~b?>B|6X z;fBK2vxX);4L7(I+9r%`5Lt#7vs|BWb>9OWii6y}TY2_B%Hl&2q*u5(8fDVNkJ$aJ z2Z|jl7@?XSyF+8SNu@Y&OSoAH)eri+zOsB@WxB@nh&fh;9m?cL=P`-PlYa}d?ib6m zo*hg4VM@O=0M@orudth+s;iU(c}SYajHe#}<$xm9vD z$wg0a8^+Pm5j0)XwumA?Uy}fz(Nx0#W682Wjy`-mfHON+Bck~KtQ5xvI{ic$WZ?psn@;tcPEW&g^j%W zvdWh)4&FC%e}_Eh(KV1*v(HY}_{H;K?%$94>E6Hk=;FrbKYD7KD$|GIB`)Vinl2;M zhhpfwY#PMcL?QGN1;8DfT>CD;lNl~|fFAY5(4&9S?k#-zXP;R?@};=w@;nDjx?SY4 z1vZ1kIJSzD$AshDFN3|XW?$?9vje#=fBkAwWfx|!B(NZrar#3S1R+2TvOq%?jspTI z15?2K7}cbhrI35z7>=q!0Cofk3DKC)p1on5+C)=Q#c)P%>EdPVZ~d{UL-z`U4NphuoXL~TQ8!HIc5%ridE;5<5T*CL$( zOVv2+SY&Qo$U5;hUoOV#%-tTJE2Zi%re zyIgOweVSi4XAQ+=Z!M`nxl0nTX?Zh+hf@eB|V1dxJ$vwfl8wlcrdU>?p9 zqikJ>E~jtTgDmv7>6&Z?Bsq}Qw=2DiTcdTQZrA8?@GsjB`b0dRyDL}NK#j4GAVv|# zDcixz>hO}|19znO+*iKmn}Pdd&T|a3iAcf`?{rUyfC4n*-iltZ_9-B>lBA@1^SpL@?b|g!}$QW&O{`U5cVCXZ&NUAbp(F@BLApaQwxJc{0K| zTw0c!fRjB8sO?q{`hOAHZ6GFct4{ye?i&2=kFV=7Y41p94}4J~xlrvi{OD!>1;f0& zkqLI7S4o$3`816n9!Hy(;nV=D@V-WB#5v#%ggzZCO7VhJW-^BUxT*#->F9WyU$RdPFAVHr<4aKg;iaDz1+ICSsn+ zQv>k@h(2hz{{px#!vKsVV38+-FKX+U&{SwIOIp3Rt zT6+%HpFqoxzU!Lbdl_uS5tc_e0dgSH@kfh>upD?LCA~n=E90|h*O(;VK}W_*?vVHt z=P`2*)IA5cnW1hTqaQvVdj2h%a&9QjMJD0p+PzheDa0+`xPMLhPn^< zO8Mq8=<%cmx1@nF1X${vD`}yY>e1QtYVuL$1wLcw?~KJ_jEfjteQZBGqmdQ{OuOYp z9pvg|B3{Vm`7~>oXG*OYXly1!;zR}sx!E=F>HX2i+>m{Nceh_CVSNM?K-auXz-`FD z2m%&fQcezulC#0mrR(RQ1IH?Ei(wJ;LsFf1_W{a%P)u?p@&Zd^`XaEP>U^f%NQpdp23F&g0IWG|f|0fDfH0C}az#@0HbS z;8MZYjq@!sT@r8vCyP1BZ1)j)c2@z@MAlm|urwwU$3P~S_Y>T#Z>}oACq-4;YvUTW zgn}$$vWAXmo&$Iv?|l8(V}AEjx{t0hvfM<2Eb@Cuv?g4ULP5Pu@LUW`Ix?-HYcjcM zz|ka+Nmst$zCnKKoz8tkZV50GL-ZgMz8s7Gm!n{)n*^&o#Gb%9+BX0fi7aHSzO$Ir z2lv|wUT&bMH{>?GQG$UW$Z>LzDbbi;Eco>=;AGvvuYVEL>m%S!ns&coQ5IEpVGS$Y zrU{;|Co3JsL{c7sT=|mDt3YJ&dguOP;f`~}{&UFha_h(GU-oRhIFnF|M_70Zf0H3{%DCB~QZqD=-Fh>~bK-=JPn;`pS zKi+$BZN6*GUij)~PEN8*U;`LLK~(7$pIuqEe=+GQ%wKX@A${#{@;HE;{rr2!r^*SwXD8wKIUam+G|wOP z>sAV&4lw8(G#=-< zNjvZ3HH%aAAXAT*=9&5aVse#Pw-tnMw zgT5ff1#}KAE?4=l4J)a_b4`(7O_wHSmIC=O%CMonzynV$!tb6&GdQm+LerhjYQa%; zN;$|kmaND?({%;oo{v89pmP9;`{Q!#)NGm)1q_140M)`wuou0c*M5(GOj_D-DKwpW zl5GP{<^AJwkOmJv@A!PD3YkqkH~|!Y__>qa@v*HE8;3!69v_0)&mUBI$HRdrr^IAZ zdJ$6=U}p=<%;=a>R0fUDeGxEX+@TkvoRlo5cv$e76;2}pN#F7n=W`97k<|PpspZv} zvUs6z^yIPE_8L>|bw_kU_luoyQ58*FP2~6Z$4Go%QT39!>#W2BP_# z`@;H_$zVDiFu_P+xpdOoz^WH3*{@5oqJgEv5c=Oz{H2c*aXaN z0M79qS-o~OzJ~o7pbg}*EnXKo&^M>}s=~(}UgY6iywaIYmQfE8nY=!H+Qn6Sz3J;- z2Htu|mQh!F)BViDjbH|O_;Q$}$!)NQ3=K1-kU0B~!c8=OzVlQ7haEd^ z*CsN)Z;&k9wtZ+RlJNMrD+B!r>n}cO(H}jgxo}`~CnnB~T|LWm*4z?jbB*X77X2$% z#lw&~0h_sw=n6mEvaW&EaEF_GKA%Fx$Bnh1YnK!ixF^0 z!ri->g-3xdwH~}rAMcCdEAkQh*g$O=%Zv| zf`i?#W=uQwAj5anAf5i5k3DXlx*K#x2(3e@qr6qv)%fZ+XS#HG*pp|>pYpf|X+7R$ zkNE6iqf3{Tqh)^l_$QqA`o*EYu{$(Dk<^VHj_&)09wZ!#c{!5C>#}>sJp9S?r77e{ z_6=$`Hle*HU9!&+Pg!sn_bboeV$K3`zc!!}Qp33+3(Ea1UVl!uWCs~}0twg$!M{(q z|4VZ=keg2%)$D|>jb7iqqPAI-wPskcmwVBVzHg;EaojLx1q%-mN!^f2Yh(eMZYsaN z2KL9Ehdh(Cb}HYew30@HJe*5$hVTo2?niA{Jpn+Gr1pKB;CaZH@<%7fkALp3v~6D~ znl%K&VF&1ec~V=;*lMI_j7VZ1(Bn6sICHpjXIDk`<+&LkY3$p`XV9#vzZyQMuuqR> z(8fK3cX~ti)Aqovl8+7=4%IBT?*G!CAD8~bS;Q67Gm&mr18Iyr%(SajhA)>7eY|pJ z@#mk>6dnhbsP}2Elm6xdXBoMg{oIt@*);@ipLpPk!1eb)Np^=e(>7KVn4#?CI3jtm zFOuc+??bgyzVE)U+$3ikSrMue?-i;#(Uf^ev4kbqnWtUu#oKmWx2^Fv|M17pub^70 z6JMNlEPA`2;A|> z@12dTjaf6=#$hA|#U6TN`PH@G=+VsFSV{cTS&-1Xk-A&%QKES3!6W&jrvfX<*~!A? zxp=8x)POw!P&??f7l4k4yIH>QBVf){YJYzO13_=idO(PRG|kD5x`rzP&8CUeTR-wU zXDxZYG5+xX)O2s*G=^?rRuAEEb96MM)UNyF*6qX=|MDkiFZn>?r%s$v*abNdMF5JV zX=-|O5`*x|aPRioLuWC8>CdnK`$%GI_CP$AriU*Pe=}w4&wucLYPC3@(aZd|;oexh=Ikbayh$u?IV*Y#R{txfk|_S_@z-Ae%2`g3xV!7TjQwC&1RxHi z-2+5KLU?fM=PPGCzZ3K)Hhw^iu67HMkqJcnl<(e23%3*y?v0#xz4P)(>ni|@ z#RA6)18~+Gwoz_rt0Bs49<*%8&Ln#y3z*x*AqtL*45{qhz|NLKlvzD!0nI@+Wd{Pc zZheeZc46^{**!!t|5^@{yu)#nC{G+V0na-%0S>L1E)Z;VlG!tC$C0$ + val diffInUnit = diffAbs / timeUnit.millis + val diffInUnitModulo = diffAbs % timeUnit.millis + val timeUnitHalf = timeUnit.millis / 2 + + if (diffInUnit >= 1 && diffInUnitModulo >= timeUnitHalf) { + return format(LocalContext.current, diffInUnit + 1, diffSign, timeUnit) + } + + if (diffInUnit >= 1 && diffInUnitModulo < timeUnitHalf) { + return format(LocalContext.current, diffInUnit, diffSign, timeUnit) + } + } + + return format(LocalContext.current, diff, diffSign, timeUnits.last()) + } + + private fun format(context: Context, quantity: Long, sign: Int, timeUnit: TimeUnit): String { + return context.resources.getQuantityString( + if (sign > 0) timeUnit.pastStringRes else timeUnit.pastStringRes, // TODO: Handle future values + quantity.toInt(), + quantity.toInt(), + ) + } + + enum class TimeUnit(val millis: Long, val pastStringRes: Int) { + Second(1_000L, R.plurals.past_duration_seconds), + Minute(60 * 1_000L, R.plurals.past_duration_minutes), + Hour(60 * 60 * 1_000L, R.plurals.past_duration_hours), + Day(24 * 60 * 60 * 1_000L, R.plurals.past_duration_days), + Week(7 * 24 * 60 * 60 * 1_000L, R.plurals.past_duration_weeks), + Month(4 * 7 * 24 * 60 * 60 * 1_000L, R.plurals.past_duration_months), + } +} \ No newline at end of file diff --git a/core/locale/src/main/java/com/twofasapp/locale/TwsLocale.kt b/core/locale/src/main/java/com/twofasapp/locale/TwsLocale.kt deleted file mode 100644 index 01a13c3a..00000000 --- a/core/locale/src/main/java/com/twofasapp/locale/TwsLocale.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.twofasapp.locale - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -object TwsLocale { - val strings: Strings - @Composable - get() = Strings(LocalContext.current) - - val links: Links = Links() -} \ No newline at end of file diff --git a/core/locale/src/main/res/values/strings-duration.xml b/core/locale/src/main/res/values/strings-duration.xml new file mode 100644 index 00000000..477c7b92 --- /dev/null +++ b/core/locale/src/main/res/values/strings-duration.xml @@ -0,0 +1,31 @@ + + + + moments ago + + + + %d minute ago + %d minutes ago + + + + %d hour ago + %d hours ago + + + + %d day ago + %d days ago + + + + %d week ago + %d weeks ago + + + + %d month ago + %d months ago + + \ No newline at end of file diff --git a/browserextension/domain/.gitignore b/core/network/.gitignore similarity index 100% rename from browserextension/domain/.gitignore rename to core/network/.gitignore diff --git a/network/build.gradle.kts b/core/network/build.gradle.kts similarity index 53% rename from network/build.gradle.kts rename to core/network/build.gradle.kts index 5120e237..13ae7b78 100644 --- a/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -9,11 +9,10 @@ android { } dependencies { - implementation(project(":di")) - implementation(project(":serialization")) - implementation(project(":prefs")) - implementation(project(":environment")) - - implementation(libs.bundles.ktor) + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(libs.kotlinCoroutines) + implementation(libs.kotlinSerialization) implementation(libs.timber) + api(libs.bundles.ktor) } \ No newline at end of file diff --git a/network/src/main/AndroidManifest.xml b/core/network/src/main/AndroidManifest.xml similarity index 73% rename from network/src/main/AndroidManifest.xml rename to core/network/src/main/AndroidManifest.xml index 5d494918..a5918e68 100644 --- a/network/src/main/AndroidManifest.xml +++ b/core/network/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/core/network/src/main/java/com/twofasapp/network/di/NetworkModule.kt b/core/network/src/main/java/com/twofasapp/network/di/NetworkModule.kt new file mode 100644 index 00000000..2fd39137 --- /dev/null +++ b/core/network/src/main/java/com/twofasapp/network/di/NetworkModule.kt @@ -0,0 +1,55 @@ +package com.twofasapp.network.di + +import com.twofasapp.common.environment.AppBuild +import com.twofasapp.di.KoinModule +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.DefaultRequest +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import org.koin.dsl.module +import timber.log.Timber + +class NetworkModule : KoinModule { + override fun provide() = module { + single { + val isDebuggable = get().isDebuggable + + HttpClient(OkHttp) { + expectSuccess = true + + if (isDebuggable) { + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + Timber.tag("Ktor").v(message) + } + } + level = LogLevel.ALL + } + } + + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + coerceInputValues = true + } + ) + } + install(DefaultRequest) { + url("https://api2.2fas.com") + contentType(ContentType.Application.Json) + } + } + } + } +} \ No newline at end of file diff --git a/di/.gitignore b/core/otp/.gitignore similarity index 100% rename from di/.gitignore rename to core/otp/.gitignore diff --git a/core/otp/build.gradle.kts b/core/otp/build.gradle.kts new file mode 100644 index 00000000..d0af90c6 --- /dev/null +++ b/core/otp/build.gradle.kts @@ -0,0 +1,13 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) +} + +android { + namespace = "com.twofasapp.otp" +} + +dependencies { +// implementation(project(":core:di")) + implementation(libs.apacheCommonsCodec) +} \ No newline at end of file diff --git a/environment/src/main/AndroidManifest.xml b/core/otp/src/main/AndroidManifest.xml similarity index 71% rename from environment/src/main/AndroidManifest.xml rename to core/otp/src/main/AndroidManifest.xml index 5518bb33..a5918e68 100644 --- a/environment/src/main/AndroidManifest.xml +++ b/core/otp/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/core/otp/src/main/java/com/twofasapp/otp/OtpAuthenticator.kt b/core/otp/src/main/java/com/twofasapp/otp/OtpAuthenticator.kt new file mode 100755 index 00000000..320c5209 --- /dev/null +++ b/core/otp/src/main/java/com/twofasapp/otp/OtpAuthenticator.kt @@ -0,0 +1,117 @@ +package com.twofasapp.otp + +import org.apache.commons.codec.binary.Base32 +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.and +import kotlin.math.pow + +/** + * This class implements the functionality described in RFC 6238 (TOTP: Time + * based one-time password algorithm) and has been tested again Google's + * implementation of such algorithm in its Google Authenticator application. + * + * This class lets users create a new 16-bit base32-encoded secret key with + * the validation code calculated at `time = 0` (the UNIX epoch) and the + * URL of a Google-provided QR barcode to let an user load the generated + * information into Google Authenticator. + * + * Java Server side class for Google Authenticator's TOTP generator was inspired by an author's blog post. + * + * @see [Blog Post](http://thegreyblog.blogspot.com/2011/12/google-authenticator-using-it-in-your.html) + * @see [Google Authenticator](http://code.google.com/p/google-authenticator) + * @see [HOTP Time Based](http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt) + */ +class OtpAuthenticator { + + companion object { + private const val HmacSha1 = "HmacSHA1" + private const val HmacSha224 = "HmacSHA224" + private const val HmacSha256 = "HmacSHA256" + private const val HmacSha384 = "HmacSHA384" + private const val HmacSha512 = "HmacSHA512" + private val keyModulus = mapOf( + 6 to 10.0.pow(6.toDouble()).toLong(), + 7 to 10.0.pow(7.toDouble()).toLong(), + 8 to 10.0.pow(8.toDouble()).toLong(), + ) + } + + fun generateOtpCode(otpData: OtpData): String { + val code = calculateCode( + key = Base32().decode(otpData.secret), + counter = otpData.counter, // ?: (otpData.nowMillis / Duration.ofSeconds(otpData.period.toLong()).toMillis()), + digits = otpData.digits, + algorithm = when (otpData.algorithm) { + OtpData.Algorithm.SHA1 -> HmacSha1 + OtpData.Algorithm.SHA224 -> HmacSha224 + OtpData.Algorithm.SHA256 -> HmacSha256 + OtpData.Algorithm.SHA384 -> HmacSha384 + OtpData.Algorithm.SHA512 -> HmacSha512 + } + ) + + return String.format("%0${otpData.digits}d", code) + } + + /** + * Calculates the verification code of the provided key at the specified + * instant of time using the algorithm specified in RFC 6238. + * + * @param key the secret key in binary format. + * @param counter the instant of time. + * @return the validation code for the provided key at the specified instant of time. + */ + private fun calculateCode( + key: ByteArray, + counter: Long, + digits: Int, + algorithm: String, + ): Int { + // Converting the instant of time from the long representation to a big-endian array of bytes (RFC4226, 5.2. Description). + val bigEndianTimestamp = ByteArray(8) + var value = counter + var byte = 8 + while (byte-- > 0) { + bigEndianTimestamp[byte] = value.toByte() + value = value ushr 8 + } + + // Building the secret key specification for the HmacSHA1 algorithm. + val signKey = SecretKeySpec(key, algorithm) + + try { + // Getting an HmacSHA1 algorithm implementation from the JCE. + val mac = Mac.getInstance(algorithm) + mac.init(signKey) + + // Processing the instant of time and getting the encrypted data. + val hash = mac.doFinal(bigEndianTimestamp) + + // Building the validation code performing dynamic truncation (RFC4226, 5.3. Generating an HOTP value) + val offset = hash[hash.size - 1] and 0xF + + // We are using a long because Java hasn't got an unsigned integer type and we need 32 unsigned bits). + var truncatedHash: Long = 0 + + for (i in 0..3) { + truncatedHash = truncatedHash shl 8 + + // Java bytes are signed but we need an unsigned integer: cleaning off all but the LSB. + truncatedHash = truncatedHash or (hash[offset + i].toInt() and 0xFF).toLong() + } + + // Clean bits higher than the 32nd (inclusive) and calculate the module with the maximum validation code value. + truncatedHash = truncatedHash and 0x7FFFFFFF + truncatedHash %= keyModulus[digits]!! + + return truncatedHash.toInt() + } catch (e: NoSuchAlgorithmException) { + throw OtpException("The operation cannot be performed now.", e) + } catch (e: InvalidKeyException) { + throw OtpException("The operation cannot be performed now.", e) + } + } +} diff --git a/core/otp/src/main/java/com/twofasapp/otp/OtpData.kt b/core/otp/src/main/java/com/twofasapp/otp/OtpData.kt new file mode 100644 index 00000000..8fbf4019 --- /dev/null +++ b/core/otp/src/main/java/com/twofasapp/otp/OtpData.kt @@ -0,0 +1,11 @@ +package com.twofasapp.otp + +data class OtpData( + val counter: Long, + val secret: String, + val digits: Int, + val period: Int, + val algorithm: Algorithm, +) { + enum class Algorithm { SHA1, SHA224, SHA256, SHA384, SHA512 } +} \ No newline at end of file diff --git a/core/otp/src/main/java/com/twofasapp/otp/OtpException.kt b/core/otp/src/main/java/com/twofasapp/otp/OtpException.kt new file mode 100755 index 00000000..60d2e608 --- /dev/null +++ b/core/otp/src/main/java/com/twofasapp/otp/OtpException.kt @@ -0,0 +1,3 @@ +package com.twofasapp.otp + +class OtpException(message: String, cause: Exception) : Exception(message, cause) \ No newline at end of file diff --git a/core/src/main/java/com/twofasapp/core/encoding/Base64.kt b/core/src/main/java/com/twofasapp/core/encoding/Base64.kt index 0e505be1..62aabb00 100644 --- a/core/src/main/java/com/twofasapp/core/encoding/Base64.kt +++ b/core/src/main/java/com/twofasapp/core/encoding/Base64.kt @@ -21,13 +21,13 @@ fun ByteArray.encodeBase64(): ByteArray { if (position + 2 < this.size) b = b or (this[position + 2].toInt() and 0xFF) else padding++ for (i in 0 until 4 - padding) { val c = b and 0xFC0000 shr 18 - output.write(table[c].toInt()) + output.write(table[c].code) b = b shl 6 } position += 3 } for (i in 0 until padding) { - output.write('='.toInt()) + output.write('='.code) } return output.toByteArray() } diff --git a/core/storage/build.gradle.kts b/core/storage/build.gradle.kts index ebc5691a..3c1ec8b2 100644 --- a/core/storage/build.gradle.kts +++ b/core/storage/build.gradle.kts @@ -9,7 +9,7 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(libs.kotlinCoroutines) implementation(libs.kotlinSerialization) implementation(libs.securityCrypto) diff --git a/core/storage/src/main/java/com/twofasapp/storage/Preferences.kt b/core/storage/src/main/java/com/twofasapp/storage/Preferences.kt index 6cb78aec..770d463e 100644 --- a/core/storage/src/main/java/com/twofasapp/storage/Preferences.kt +++ b/core/storage/src/main/java/com/twofasapp/storage/Preferences.kt @@ -1,5 +1,7 @@ package com.twofasapp.storage +import kotlinx.coroutines.flow.Flow + interface Preferences { fun getBoolean(key: String): Boolean? fun getString(key: String): String? @@ -13,5 +15,7 @@ interface Preferences { fun putInt(key: String, value: Int) fun putFloat(key: String, value: Float) + fun observe(key: String, default: T): Flow + fun delete(key: String) } \ No newline at end of file diff --git a/core/storage/src/main/java/com/twofasapp/storage/di/PreferencesModule.kt b/core/storage/src/main/java/com/twofasapp/storage/di/StorageModule.kt similarity index 96% rename from core/storage/src/main/java/com/twofasapp/storage/di/PreferencesModule.kt rename to core/storage/src/main/java/com/twofasapp/storage/di/StorageModule.kt index a91539c1..519836d4 100644 --- a/core/storage/src/main/java/com/twofasapp/storage/di/PreferencesModule.kt +++ b/core/storage/src/main/java/com/twofasapp/storage/di/StorageModule.kt @@ -11,7 +11,7 @@ import com.twofasapp.storage.internal.PlainSharedPreferencesFactory import org.koin.android.ext.koin.androidContext import org.koin.dsl.module -class PreferencesModule : KoinModule { +class StorageModule : KoinModule { override fun provide() = module { single { diff --git a/core/storage/src/main/java/com/twofasapp/storage/internal/PreferencesDelegate.kt b/core/storage/src/main/java/com/twofasapp/storage/internal/PreferencesDelegate.kt index 35fab368..7f74c476 100644 --- a/core/storage/src/main/java/com/twofasapp/storage/internal/PreferencesDelegate.kt +++ b/core/storage/src/main/java/com/twofasapp/storage/internal/PreferencesDelegate.kt @@ -3,6 +3,12 @@ package com.twofasapp.storage.internal import android.content.SharedPreferences import com.twofasapp.storage.EncryptedPreferences import com.twofasapp.storage.PlainPreferences +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import timber.log.Timber internal class PreferencesDelegate( @@ -13,6 +19,31 @@ internal class PreferencesDelegate( factory.create() } + private val flow by lazy { + callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) } + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + } + } + + override fun observe(key: String, default: T): Flow { + return flow + .filter { it == key || it == null } + .map { + @Suppress("UNCHECKED_CAST") + when (default) { + is String -> getString(key) as T? + is Int -> getInt(key) as T? + is Long -> getLong(key) as T? + is Boolean -> getBoolean(key) as T? + is Float -> getFloat(key) as T? + else -> throw IllegalArgumentException("Unsupported preference flow type") + } ?: default + } + .conflate() + } + override fun getBoolean(key: String): Boolean? { return getIfExists(key) { sharedPrefs.getBoolean(key, false) } } diff --git a/environment/.gitignore b/data/browserext/.gitignore similarity index 100% rename from environment/.gitignore rename to data/browserext/.gitignore diff --git a/data/browserext/build.gradle.kts b/data/browserext/build.gradle.kts new file mode 100644 index 00000000..98d26a60 --- /dev/null +++ b/data/browserext/build.gradle.kts @@ -0,0 +1,21 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) + alias(libs.plugins.kotlinSerialization) +} + +android { + namespace = "com.twofasapp.data.browserext" +} + +dependencies { + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(project(":core:storage")) + implementation(project(":core:network")) + + implementation(libs.bundles.room) + implementation(libs.kotlinCoroutines) + implementation(libs.kotlinSerialization) + implementation(libs.timber) +} \ No newline at end of file diff --git a/notifications/src/main/AndroidManifest.xml b/data/browserext/src/main/AndroidManifest.xml similarity index 70% rename from notifications/src/main/AndroidManifest.xml rename to data/browserext/src/main/AndroidManifest.xml index 09e2ebfa..a5918e68 100644 --- a/notifications/src/main/AndroidManifest.xml +++ b/data/browserext/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepository.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepository.kt similarity index 76% rename from browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepository.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepository.kt index e6203d73..3c7ca962 100644 --- a/browserextension/src/main/java/com/twofasapp/browserextension/domain/repository/BrowserExtensionRepository.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepository.kt @@ -1,12 +1,11 @@ -package com.twofasapp.browserextension.domain.repository +package com.twofasapp.data.browserext -import com.twofasapp.browserextension.domain.model.MobileDevice -import com.twofasapp.browserextension.domain.model.PairedBrowser -import com.twofasapp.browserextension.domain.model.TokenRequest +import com.twofasapp.data.browserext.domain.MobileDevice +import com.twofasapp.data.browserext.domain.PairedBrowser +import com.twofasapp.data.browserext.domain.TokenRequest import kotlinx.coroutines.flow.Flow -internal interface BrowserExtensionRepository { - +interface BrowserExtRepository { fun observeMobileDevice(): Flow fun observePairedBrowsers(): Flow> diff --git a/data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepositoryImpl.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepositoryImpl.kt new file mode 100644 index 00000000..bf1e81e2 --- /dev/null +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/BrowserExtRepositoryImpl.kt @@ -0,0 +1,100 @@ +package com.twofasapp.data.browserext + +import com.twofasapp.data.browserext.domain.MobileDevice +import com.twofasapp.data.browserext.domain.PairedBrowser +import com.twofasapp.data.browserext.domain.TokenRequest +import com.twofasapp.data.browserext.local.BrowserExtLocalSource +import com.twofasapp.data.browserext.mapper.asDomain +import com.twofasapp.data.browserext.remote.BrowserExtRemoteSource +import com.twofasapp.data.browserext.remote.model.ApproveLoginRequestBody +import com.twofasapp.data.browserext.remote.model.PairBrowserBody +import com.twofasapp.data.browserext.remote.model.RegisterDeviceBody +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +internal class BrowserExtRepositoryImpl( + private val localSource: BrowserExtLocalSource, + private val remoteSource: BrowserExtRemoteSource, +) : BrowserExtRepository { + + companion object { + private const val PLATFORM = "android" + } + + override fun observeMobileDevice(): Flow { + return localSource.observeMobileDevice() + } + + override fun observePairedBrowsers(): Flow> { + return localSource.observePairedBrowsers() + } + + override suspend fun updateMobileDevice(mobileDevice: MobileDevice) { + remoteSource.updateMobileDevice(mobileDevice.id, mobileDevice.name) + localSource.saveMobileDevice(mobileDevice) + } + + override suspend fun registerMobileDevice(deviceName: String, devicePublicKey: String, fcmToken: String): MobileDevice { + val mobileDevice = remoteSource.registerMobileDevice( + devicePublicKey = devicePublicKey, + body = RegisterDeviceBody( + name = deviceName, + fcm_token = fcmToken, + platform = PLATFORM, + ) + ) + + localSource.saveMobileDevice(mobileDevice) + return mobileDevice + } + + override suspend fun pairBrowser(deviceId: String, extensionId: String, deviceName: String, devicePublicKey: String): PairedBrowser { + val browser = remoteSource.pairBrowser( + deviceId = deviceId, + body = PairBrowserBody( + extension_id = extensionId, + device_name = deviceName, + device_public_key = devicePublicKey, + ) + ) + localSource.savePairedBrowser(browser) + return browser + } + + override suspend fun updatePairedBrowser(extensionId: String, newName: String) { + remoteSource.updateBrowserName(extensionId = extensionId, newName = newName) + fetchPairedBrowsers() + } + + override suspend fun fetchPairedBrowsers() { + val id = localSource.observeMobileDevice().first().id + + if (id.isNotBlank()) { + localSource.updatePairedBrowsers(remoteSource.getBrowsers(id).map { it.asDomain() }) + } + } + + override suspend fun fetchTokenRequests(deviceId: String): List { + return remoteSource.fetchTokenRequests(deviceId).map { it.asDomain() } + } + + override suspend fun deletePairedBrowser(deviceId: String, extensionId: String) { + remoteSource.deletePairedBrowser(deviceId, extensionId) + fetchPairedBrowsers() + } + + override suspend fun acceptLoginRequest(deviceId: String, extensionId: String, requestId: String, code: String) { + return remoteSource.acceptLoginRequest( + deviceId, + body = ApproveLoginRequestBody( + extension_id = extensionId, + token_request_id = requestId, + token = code, + ) + ) + } + + override suspend fun denyLoginRequest(extensionId: String, requestId: String) { + return remoteSource.denyLoginRequest(extensionId, requestId) + } +} \ No newline at end of file diff --git a/data/browserext/src/main/java/com/twofasapp/data/browserext/di/DataBrowserExtModule.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/di/DataBrowserExtModule.kt new file mode 100644 index 00000000..47626861 --- /dev/null +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/di/DataBrowserExtModule.kt @@ -0,0 +1,18 @@ +package com.twofasapp.data.browserext.di + +import com.twofasapp.data.browserext.BrowserExtRepository +import com.twofasapp.data.browserext.BrowserExtRepositoryImpl +import com.twofasapp.data.browserext.local.BrowserExtLocalSource +import com.twofasapp.data.browserext.remote.BrowserExtRemoteSource +import com.twofasapp.di.KoinModule +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +class DataBrowserExtModule : KoinModule { + override fun provide() = module { + singleOf(::BrowserExtLocalSource) + singleOf(::BrowserExtRemoteSource) + singleOf(::BrowserExtRepositoryImpl) { bind() } + } +} \ No newline at end of file diff --git a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/MobileDevice.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/domain/MobileDevice.kt similarity index 74% rename from browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/MobileDevice.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/domain/MobileDevice.kt index 01422282..6a26f2ff 100644 --- a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/MobileDevice.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/domain/MobileDevice.kt @@ -1,4 +1,4 @@ -package com.twofasapp.browserextension.domain.model +package com.twofasapp.data.browserext.domain data class MobileDevice( val id: String, diff --git a/data/browserext/src/main/java/com/twofasapp/data/browserext/domain/PairedBrowser.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/domain/PairedBrowser.kt new file mode 100644 index 00000000..a49d15d8 --- /dev/null +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/domain/PairedBrowser.kt @@ -0,0 +1,10 @@ +package com.twofasapp.data.browserext.domain + +import java.time.Instant + +data class PairedBrowser( + val id: String, + val name: String, + val pairedAt: Instant, + val extensionPublicKey: String, +) \ No newline at end of file diff --git a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/TokenRequest.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/domain/TokenRequest.kt similarity index 67% rename from browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/TokenRequest.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/domain/TokenRequest.kt index dd823914..d1a4c149 100644 --- a/browserextension/domain/src/main/java/com/twofasapp/browserextension/domain/model/TokenRequest.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/domain/TokenRequest.kt @@ -1,4 +1,4 @@ -package com.twofasapp.browserextension.domain.model +package com.twofasapp.data.browserext.domain data class TokenRequest( val domain: String, diff --git a/data/browserext/src/main/java/com/twofasapp/data/browserext/local/BrowserExtLocalSource.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/BrowserExtLocalSource.kt new file mode 100644 index 00000000..b1bcca95 --- /dev/null +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/BrowserExtLocalSource.kt @@ -0,0 +1,78 @@ +package com.twofasapp.data.browserext.local + +import com.twofasapp.data.browserext.domain.MobileDevice +import com.twofasapp.data.browserext.domain.PairedBrowser +import com.twofasapp.data.browserext.local.model.MobileDeviceEntity +import com.twofasapp.data.browserext.local.model.PairedBrowserEntity +import com.twofasapp.data.browserext.mapper.asDomain +import com.twofasapp.data.browserext.mapper.asEntity +import com.twofasapp.storage.PlainPreferences +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onSubscription +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.time.Instant + +internal class BrowserExtLocalSource( + private val json: Json, + private val dao: PairedBrowserDao, + private val preferences: PlainPreferences, +) { + companion object { + private const val KeyMobileDevice = "mobileDevice" + } + + private val mobileDeviceFlow: MutableSharedFlow = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + fun observeMobileDevice(): Flow { + return mobileDeviceFlow.onSubscription { + val device = preferences.getString(KeyMobileDevice)?.let { + json.decodeFromString(it) + } ?: MobileDeviceEntity(id = "", name = "", fcmToken = "", platform = "", publicKey = "") + + emit(device.asDomain()) + } + } + + fun observePairedBrowsers(): Flow> { + return dao.observe() + .map { list -> list.map { it.toDomain() } } + } + + suspend fun saveMobileDevice(mobileDevice: MobileDevice) { + preferences.putString(KeyMobileDevice, json.encodeToString(mobileDevice.asEntity())) + mobileDeviceFlow.tryEmit(mobileDevice) + } + + suspend fun savePairedBrowser(pairedBrowser: PairedBrowser) { + dao.insertOrUpdate(pairedBrowser.toEntity()) + } + + suspend fun updatePairedBrowsers(pairedBrowsers: List) { + dao.updateAll(pairedBrowsers.map { it.toEntity() }) + } + + private fun PairedBrowser.toEntity() = + PairedBrowserEntity( + id = id, + name = name, + extensionPublicKey = extensionPublicKey, + pairedAt = pairedAt.toEpochMilli(), + ) + + private fun PairedBrowserEntity.toDomain() = + PairedBrowser( + id = id, + name = name, + pairedAt = Instant.ofEpochMilli(pairedAt), + extensionPublicKey = extensionPublicKey, + ) +} \ No newline at end of file diff --git a/persistence/src/main/java/com/twofasapp/persistence/dao/PairedBrowserDao.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/PairedBrowserDao.kt similarity index 88% rename from persistence/src/main/java/com/twofasapp/persistence/dao/PairedBrowserDao.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/local/PairedBrowserDao.kt index 5a6e460f..a140f606 100644 --- a/persistence/src/main/java/com/twofasapp/persistence/dao/PairedBrowserDao.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/PairedBrowserDao.kt @@ -1,7 +1,7 @@ -package com.twofasapp.persistence.dao +package com.twofasapp.data.browserext.local import androidx.room.* -import com.twofasapp.persistence.model.PairedBrowserEntity +import com.twofasapp.data.browserext.local.model.PairedBrowserEntity import kotlinx.coroutines.flow.Flow @Dao diff --git a/prefs/src/main/java/com/twofasapp/prefs/model/MobileDeviceEntity.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/model/MobileDeviceEntity.kt similarity index 66% rename from prefs/src/main/java/com/twofasapp/prefs/model/MobileDeviceEntity.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/local/model/MobileDeviceEntity.kt index f75ea6ef..22977b72 100644 --- a/prefs/src/main/java/com/twofasapp/prefs/model/MobileDeviceEntity.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/model/MobileDeviceEntity.kt @@ -1,9 +1,9 @@ -package com.twofasapp.prefs.model +package com.twofasapp.data.browserext.local.model import kotlinx.serialization.Serializable @Serializable -data class MobileDeviceEntity( +internal data class MobileDeviceEntity( val id: String, val name: String, val fcmToken: String, diff --git a/persistence/src/main/java/com/twofasapp/persistence/model/PairedBrowserEntity.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/model/PairedBrowserEntity.kt similarity index 83% rename from persistence/src/main/java/com/twofasapp/persistence/model/PairedBrowserEntity.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/local/model/PairedBrowserEntity.kt index 4ed12eda..ffb8829a 100644 --- a/persistence/src/main/java/com/twofasapp/persistence/model/PairedBrowserEntity.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/local/model/PairedBrowserEntity.kt @@ -1,4 +1,4 @@ -package com.twofasapp.persistence.model +package com.twofasapp.data.browserext.local.model import androidx.room.Entity import androidx.room.PrimaryKey diff --git a/data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/MobileDeviceMapper.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/MobileDeviceMapper.kt new file mode 100644 index 00000000..532356f6 --- /dev/null +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/MobileDeviceMapper.kt @@ -0,0 +1,20 @@ +package com.twofasapp.data.browserext.mapper + +import com.twofasapp.data.browserext.domain.MobileDevice +import com.twofasapp.data.browserext.local.model.MobileDeviceEntity + +internal fun MobileDevice.asEntity() = MobileDeviceEntity( + id = id, + name = name, + fcmToken = fcmToken, + platform = platform, + publicKey = publicKey, +) + +internal fun MobileDeviceEntity.asDomain() = MobileDevice( + id = id, + name = name, + fcmToken = fcmToken, + platform = platform, + publicKey = publicKey, +) \ No newline at end of file diff --git a/data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/PairBrowserMapper.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/PairBrowserMapper.kt new file mode 100644 index 00000000..c7d61985 --- /dev/null +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/mapper/PairBrowserMapper.kt @@ -0,0 +1,24 @@ +package com.twofasapp.data.browserext.mapper + +import com.twofasapp.data.browserext.domain.PairedBrowser +import com.twofasapp.data.browserext.domain.TokenRequest +import com.twofasapp.data.browserext.remote.model.BrowserJson +import com.twofasapp.data.browserext.remote.model.TokenRequestJson +import java.time.Instant + +internal fun TokenRequestJson.asDomain(): TokenRequest { + return TokenRequest( + domain = domain, + requestId = token_request_id, + extensionId = extension_id + ) +} + +internal fun BrowserJson.asDomain(): PairedBrowser { + return PairedBrowser( + id = id, + name = name, + pairedAt = Instant.parse(paired_at), + extensionPublicKey = "", + ) +} \ No newline at end of file diff --git a/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/BrowserExtRemoteSource.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/BrowserExtRemoteSource.kt new file mode 100644 index 00000000..c5e58d50 --- /dev/null +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/BrowserExtRemoteSource.kt @@ -0,0 +1,100 @@ +package com.twofasapp.data.browserext.remote + +import com.twofasapp.data.browserext.domain.MobileDevice +import com.twofasapp.data.browserext.domain.PairedBrowser +import com.twofasapp.data.browserext.remote.exception.BrowserAlreadyPairedException +import com.twofasapp.data.browserext.remote.model.ApproveLoginRequestBody +import com.twofasapp.data.browserext.remote.model.BrowserJson +import com.twofasapp.data.browserext.remote.model.DenyLoginRequestBody +import com.twofasapp.data.browserext.remote.model.PairBrowserBody +import com.twofasapp.data.browserext.remote.model.PairBrowserJson +import com.twofasapp.data.browserext.remote.model.RegisterDeviceBody +import com.twofasapp.data.browserext.remote.model.RegisterDeviceJson +import com.twofasapp.data.browserext.remote.model.TokenRequestJson +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.put +import io.ktor.client.request.setBody +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import java.time.Instant + +internal class BrowserExtRemoteSource( + private val client: HttpClient, +) { + + suspend fun registerMobileDevice(devicePublicKey: String, body: RegisterDeviceBody): MobileDevice { + val response: RegisterDeviceJson = client.post("/mobile/devices") { setBody(body) }.body() + + return MobileDevice( + id = response.id, + name = response.name, + fcmToken = body.fcm_token, + platform = response.platform, + publicKey = devicePublicKey, + ) + } + + suspend fun updateMobileDevice(deviceId: String, newName: String) { + client.put("/mobile/devices/$deviceId") { + setBody(buildJsonObject { put("name", JsonPrimitive(newName)) }) + }.body() + } + + suspend fun pairBrowser(deviceId: String, body: PairBrowserBody): PairedBrowser { + return try { + val pairResponse: PairBrowserJson = client.post("/mobile/devices/$deviceId/browser_extensions") { setBody(body) }.body() + + val browserResponse = getBrowser(deviceId = deviceId, extensionId = body.extension_id) + + PairedBrowser( + id = browserResponse.id, + name = browserResponse.name, + pairedAt = Instant.parse(browserResponse.paired_at), + extensionPublicKey = pairResponse.extension_public_key + ) + } catch (e: Exception) { + throw when { + e is ClientRequestException && e.response.status.value == 409 -> BrowserAlreadyPairedException() + else -> e + } + } + } + + suspend fun updateBrowserName(extensionId: String, newName: String) { + client.put("/browser_extensions/$extensionId") { + setBody(buildJsonObject { put("name", JsonPrimitive(newName)) }) + }.body() + } + + suspend fun deletePairedBrowser(deviceId: String, extensionId: String) { + return client.delete("/mobile/devices/$deviceId/browser_extensions/${extensionId}").body() + } + + suspend fun getBrowser(deviceId: String, extensionId: String): BrowserJson { + return client.get("/mobile/devices/$deviceId/browser_extensions/${extensionId}").body() + } + + suspend fun getBrowsers(deviceId: String): List { + return client.get("/mobile/devices/$deviceId/browser_extensions").body() + } + + suspend fun acceptLoginRequest(deviceId: String, body: ApproveLoginRequestBody) { + client.post("/mobile/devices/$deviceId/commands/send_2fa_token") { setBody(body) }.body() + } + + suspend fun denyLoginRequest(extensionId: String, tokenRequestId: String) { + client.post("/browser_extensions/$extensionId/2fa_requests/$tokenRequestId/commands/close_2fa_request") { setBody(DenyLoginRequestBody()) } + .body() + } + + suspend fun fetchTokenRequests(deviceId: String): List { + return client.get("/mobile/devices/$deviceId/browser_extensions/2fa_requests").body>() + .filter { it.status.equals("pending", true) } + } + +} \ No newline at end of file diff --git a/network/src/main/java/com/twofasapp/network/exception/BrowserAlreadyPairedException.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/exception/BrowserAlreadyPairedException.kt similarity index 50% rename from network/src/main/java/com/twofasapp/network/exception/BrowserAlreadyPairedException.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/exception/BrowserAlreadyPairedException.kt index f06921b3..044457b3 100644 --- a/network/src/main/java/com/twofasapp/network/exception/BrowserAlreadyPairedException.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/exception/BrowserAlreadyPairedException.kt @@ -1,3 +1,3 @@ -package com.twofasapp.network.exception +package com.twofasapp.data.browserext.remote.exception class BrowserAlreadyPairedException : RuntimeException() \ No newline at end of file diff --git a/network/src/main/java/com/twofasapp/network/body/ApproveLoginRequestBody.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/ApproveLoginRequestBody.kt similarity index 60% rename from network/src/main/java/com/twofasapp/network/body/ApproveLoginRequestBody.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/ApproveLoginRequestBody.kt index 04570f39..cf68535c 100644 --- a/network/src/main/java/com/twofasapp/network/body/ApproveLoginRequestBody.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/ApproveLoginRequestBody.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.body +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -class ApproveLoginRequestBody( +internal data class ApproveLoginRequestBody( val extension_id: String, val token_request_id: String, val token: String, diff --git a/network/src/main/java/com/twofasapp/network/response/BrowserResponse.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/BrowserJson.kt similarity index 60% rename from network/src/main/java/com/twofasapp/network/response/BrowserResponse.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/BrowserJson.kt index b2120ea5..24d08e96 100644 --- a/network/src/main/java/com/twofasapp/network/response/BrowserResponse.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/BrowserJson.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.response +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -class BrowserResponse( +internal data class BrowserJson( val id: String, val name: String, val paired_at: String, diff --git a/network/src/main/java/com/twofasapp/network/body/DenyLoginRequestBody.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/DenyLoginRequestBody.kt similarity index 50% rename from network/src/main/java/com/twofasapp/network/body/DenyLoginRequestBody.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/DenyLoginRequestBody.kt index a1057c40..33f2b0db 100644 --- a/network/src/main/java/com/twofasapp/network/body/DenyLoginRequestBody.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/DenyLoginRequestBody.kt @@ -1,8 +1,8 @@ -package com.twofasapp.network.body +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -class DenyLoginRequestBody( +internal data class DenyLoginRequestBody( val status: String = "completed" ) \ No newline at end of file diff --git a/network/src/main/java/com/twofasapp/network/body/PairBrowserBody.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/PairBrowserBody.kt similarity index 63% rename from network/src/main/java/com/twofasapp/network/body/PairBrowserBody.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/PairBrowserBody.kt index 6b15e182..cdefe4c7 100644 --- a/network/src/main/java/com/twofasapp/network/body/PairBrowserBody.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/PairBrowserBody.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.body +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -class PairBrowserBody( +internal data class PairBrowserBody( val extension_id: String, val device_name: String, val device_public_key: String, diff --git a/network/src/main/java/com/twofasapp/network/response/PairBrowserResponse.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/PairBrowserJson.kt similarity index 64% rename from network/src/main/java/com/twofasapp/network/response/PairBrowserResponse.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/PairBrowserJson.kt index 8a42eeb2..b03c6f55 100644 --- a/network/src/main/java/com/twofasapp/network/response/PairBrowserResponse.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/PairBrowserJson.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.response +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -class PairBrowserResponse( +internal data class PairBrowserJson( val extension_id: String, val extension_name: String, val extension_public_key: String, diff --git a/network/src/main/java/com/twofasapp/network/body/DeviceRegisterBody.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceBody.kt similarity index 59% rename from network/src/main/java/com/twofasapp/network/body/DeviceRegisterBody.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceBody.kt index 85a21173..209f9549 100644 --- a/network/src/main/java/com/twofasapp/network/body/DeviceRegisterBody.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceBody.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.body +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -class DeviceRegisterBody( +internal data class RegisterDeviceBody( val name: String, val fcm_token: String, val platform: String, diff --git a/network/src/main/java/com/twofasapp/network/response/DeviceRegisterResponse.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceJson.kt similarity index 66% rename from network/src/main/java/com/twofasapp/network/response/DeviceRegisterResponse.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceJson.kt index 5ec1da15..981e303a 100644 --- a/network/src/main/java/com/twofasapp/network/response/DeviceRegisterResponse.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/RegisterDeviceJson.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.response +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -class DeviceRegisterResponse( +internal data class RegisterDeviceJson( val id: String, val name: String, val platform: String, diff --git a/network/src/main/java/com/twofasapp/network/response/TokenRequestResponse.kt b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/TokenRequestJson.kt similarity index 65% rename from network/src/main/java/com/twofasapp/network/response/TokenRequestResponse.kt rename to data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/TokenRequestJson.kt index 3112a747..8726f0ea 100644 --- a/network/src/main/java/com/twofasapp/network/response/TokenRequestResponse.kt +++ b/data/browserext/src/main/java/com/twofasapp/data/browserext/remote/model/TokenRequestJson.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.response +package com.twofasapp.data.browserext.remote.model import kotlinx.serialization.Serializable @Serializable -data class TokenRequestResponse( +internal data class TokenRequestJson( val extension_id: String, val token_request_id: String, val domain: String, diff --git a/externalimport/.gitignore b/data/notifications/.gitignore similarity index 100% rename from externalimport/.gitignore rename to data/notifications/.gitignore diff --git a/data/notifications/build.gradle.kts b/data/notifications/build.gradle.kts new file mode 100644 index 00000000..14bdf324 --- /dev/null +++ b/data/notifications/build.gradle.kts @@ -0,0 +1,20 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) + alias(libs.plugins.kotlinSerialization) +} + +android { + namespace = "com.twofasapp.data.notifications" +} + +dependencies { + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(project(":core:network")) + implementation(project(":core:storage")) + + implementation(libs.bundles.room) + implementation(libs.kotlinCoroutines) + implementation(libs.kotlinSerialization) +} \ No newline at end of file diff --git a/data/notifications/src/main/AndroidManifest.xml b/data/notifications/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/data/notifications/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepository.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepository.kt new file mode 100644 index 00000000..aa2073ca --- /dev/null +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepository.kt @@ -0,0 +1,11 @@ +package com.twofasapp.data.notifications + +import com.twofasapp.data.notifications.domain.Notification +import kotlinx.coroutines.flow.Flow + +interface NotificationsRepository { + suspend fun getNotifications(): List + suspend fun fetchNotifications() + suspend fun readAllNotifications() + fun hasUnreadNotifications(): Flow +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepositoryImpl.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepositoryImpl.kt new file mode 100644 index 00000000..61a1dd99 --- /dev/null +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/NotificationsRepositoryImpl.kt @@ -0,0 +1,54 @@ +package com.twofasapp.data.notifications + +import com.twofasapp.common.coroutines.Dispatchers +import com.twofasapp.common.time.TimeProvider +import com.twofasapp.data.notifications.domain.Notification +import com.twofasapp.data.notifications.local.NotificationsLocalSource +import com.twofasapp.data.notifications.mappper.asDomain +import com.twofasapp.data.notifications.remote.NotificationsRemoteSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.time.Duration + +internal class NotificationsRepositoryImpl( + private val dispatchers: Dispatchers, + private val local: NotificationsLocalSource, + private val remote: NotificationsRemoteSource, + private val timeProvider: TimeProvider, +) : NotificationsRepository { + + companion object { + private const val publishedAfterDays = 90L + } + + override suspend fun getNotifications(): List { + return withContext(dispatchers.io) { + local.getNotifications().filterOutTooOldNotifications() + } + } + + override suspend fun fetchNotifications() { + withContext(dispatchers.io) { + val remoteData = remote.fetchNotifications(timeProvider.currentDateTimeUtc().minusDays(publishedAfterDays)) + local.saveNotifications(remoteData.map { it.asDomain() }) + } + } + + override suspend fun readAllNotifications() { + withContext(dispatchers.io) { + local.readAllNotifications() + } + } + + override fun hasUnreadNotifications(): Flow { + return local.observeNotifications() + .map { it.filterOutTooOldNotifications() } + .map { list -> list.any { it.isRead.not() } } + } + + private fun List.filterOutTooOldNotifications(): List { + return filter { it.publishTime > timeProvider.systemCurrentTime() - Duration.ofDays(publishedAfterDays).toMillis() } + .sortedWith(compareBy({ it.isRead }, { it.publishTime.unaryMinus() })) + } +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/twofasapp/data/notifications/di/DataNotificationsModule.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/di/DataNotificationsModule.kt new file mode 100644 index 00000000..15fcc29e --- /dev/null +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/di/DataNotificationsModule.kt @@ -0,0 +1,18 @@ +package com.twofasapp.data.notifications.di + +import com.twofasapp.data.notifications.NotificationsRepository +import com.twofasapp.data.notifications.NotificationsRepositoryImpl +import com.twofasapp.data.notifications.local.NotificationsLocalSource +import com.twofasapp.data.notifications.remote.NotificationsRemoteSource +import com.twofasapp.di.KoinModule +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +class DataNotificationsModule : KoinModule { + override fun provide() = module { + singleOf(::NotificationsLocalSource) + singleOf(::NotificationsRemoteSource) + singleOf(::NotificationsRepositoryImpl) { bind() } + } +} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/model/Notification.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/domain/Notification.kt similarity index 81% rename from notifications/src/main/java/com/twofasapp/notifications/domain/model/Notification.kt rename to data/notifications/src/main/java/com/twofasapp/data/notifications/domain/Notification.kt index 7dbc810a..dc511133 100644 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/model/Notification.kt +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/domain/Notification.kt @@ -1,6 +1,6 @@ -package com.twofasapp.notifications.domain.model +package com.twofasapp.data.notifications.domain -internal data class Notification( +data class Notification( val id: String, val category: Category, val link: String, diff --git a/persistence/src/main/java/com/twofasapp/persistence/dao/NotificationDao.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsDao.kt similarity index 87% rename from persistence/src/main/java/com/twofasapp/persistence/dao/NotificationDao.kt rename to data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsDao.kt index fd73e845..2f36bc47 100644 --- a/persistence/src/main/java/com/twofasapp/persistence/dao/NotificationDao.kt +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsDao.kt @@ -1,11 +1,11 @@ -package com.twofasapp.persistence.dao +package com.twofasapp.data.notifications.local import androidx.room.* -import com.twofasapp.persistence.model.NotificationEntity +import com.twofasapp.data.notifications.local.model.NotificationEntity import kotlinx.coroutines.flow.Flow @Dao -interface NotificationDao { +interface NotificationsDao { @Query("SELECT * FROM notifications") suspend fun select(): List diff --git a/data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsLocalSource.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsLocalSource.kt new file mode 100644 index 00000000..40b7a93d --- /dev/null +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/local/NotificationsLocalSource.kt @@ -0,0 +1,36 @@ +package com.twofasapp.data.notifications.local + +import com.twofasapp.data.notifications.domain.Notification +import com.twofasapp.data.notifications.mappper.asDomain +import com.twofasapp.data.notifications.mappper.asEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class NotificationsLocalSource( + private val notificationsDao: NotificationsDao, +) { + + suspend fun getNotifications(): List { + return notificationsDao.select().map { it.asDomain() } + } + + fun observeNotifications(): Flow> { + return notificationsDao.observe().map { list -> + list.map { it.asDomain() } + } + } + + suspend fun saveNotifications(notifications: List) { + notificationsDao.upsert(notifications.map { it.asEntity() }) + } + + suspend fun deleteNotifications(ids: List) { + notificationsDao.delete(ids) + } + + suspend fun readAllNotifications() { + notificationsDao.update( + *notificationsDao.select().map { it.copy(isRead = true) }.toTypedArray() + ) + } +} diff --git a/persistence/src/main/java/com/twofasapp/persistence/model/NotificationEntity.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/local/model/NotificationEntity.kt similarity index 86% rename from persistence/src/main/java/com/twofasapp/persistence/model/NotificationEntity.kt rename to data/notifications/src/main/java/com/twofasapp/data/notifications/local/model/NotificationEntity.kt index bb364f01..11da0a42 100644 --- a/persistence/src/main/java/com/twofasapp/persistence/model/NotificationEntity.kt +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/local/model/NotificationEntity.kt @@ -1,4 +1,4 @@ -package com.twofasapp.persistence.model +package com.twofasapp.data.notifications.local.model import androidx.room.Entity import androidx.room.PrimaryKey diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/converter/NotificationConverter.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/mappper/NotificationsMapper.kt similarity index 54% rename from notifications/src/main/java/com/twofasapp/notifications/domain/converter/NotificationConverter.kt rename to data/notifications/src/main/java/com/twofasapp/data/notifications/mappper/NotificationsMapper.kt index fe10a6ef..476433af 100644 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/converter/NotificationConverter.kt +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/mappper/NotificationsMapper.kt @@ -1,11 +1,10 @@ -package com.twofasapp.notifications.domain.converter +package com.twofasapp.data.notifications.mappper -import com.twofasapp.network.response.NotificationResponse -import com.twofasapp.notifications.domain.model.Notification -import com.twofasapp.persistence.model.NotificationEntity -import java.time.OffsetDateTime +import com.twofasapp.data.notifications.domain.Notification +import com.twofasapp.data.notifications.local.model.NotificationEntity +import com.twofasapp.data.notifications.remote.model.NotificationJson -internal fun Notification.toEntity() = NotificationEntity( +internal fun Notification.asEntity() = NotificationEntity( id = id, category = category.name, link = link, @@ -16,7 +15,7 @@ internal fun Notification.toEntity() = NotificationEntity( isRead = isRead, ) -internal fun NotificationEntity.toDomain() = Notification( +internal fun NotificationEntity.asDomain() = Notification( id = id, category = Notification.Category.valueOf(category), link = link, @@ -27,12 +26,12 @@ internal fun NotificationEntity.toDomain() = Notification( isRead = isRead, ) -internal fun NotificationResponse.toDomain() = Notification( +internal fun NotificationJson.asDomain() = Notification( id = id, category = Notification.Category.values().find { category -> icon == category.icon } ?: Notification.Category.News, link = link, message = message, - publishTime = OffsetDateTime.parse(published_at).toInstant().toEpochMilli(), + publishTime = java.time.OffsetDateTime.parse(published_at).toInstant().toEpochMilli(), push = push, platform = platform, isRead = false, diff --git a/data/notifications/src/main/java/com/twofasapp/data/notifications/remote/NotificationsRemoteSource.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/remote/NotificationsRemoteSource.kt new file mode 100644 index 00000000..1b17ea84 --- /dev/null +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/remote/NotificationsRemoteSource.kt @@ -0,0 +1,21 @@ +package com.twofasapp.data.notifications.remote + +import com.twofasapp.data.notifications.remote.model.NotificationJson +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +internal class NotificationsRemoteSource( + private val client: HttpClient, +) { + + suspend fun fetchNotifications(publishedAfter: OffsetDateTime): List { + return client.get("/mobile/notifications") { + parameter("platform", "android") + parameter("published_after", publishedAfter.format(DateTimeFormatter.ISO_INSTANT)) + }.body() + } +} diff --git a/network/src/main/java/com/twofasapp/network/response/NotificationResponse.kt b/data/notifications/src/main/java/com/twofasapp/data/notifications/remote/model/NotificationJson.kt similarity index 72% rename from network/src/main/java/com/twofasapp/network/response/NotificationResponse.kt rename to data/notifications/src/main/java/com/twofasapp/data/notifications/remote/model/NotificationJson.kt index 12bd1e41..b28eaad6 100644 --- a/network/src/main/java/com/twofasapp/network/response/NotificationResponse.kt +++ b/data/notifications/src/main/java/com/twofasapp/data/notifications/remote/model/NotificationJson.kt @@ -1,9 +1,9 @@ -package com.twofasapp.network.response +package com.twofasapp.data.notifications.remote.model import kotlinx.serialization.Serializable @Serializable -class NotificationResponse( +internal class NotificationJson( val id: String, val icon: String, val link: String, diff --git a/network/.gitignore b/data/services/.gitignore similarity index 100% rename from network/.gitignore rename to data/services/.gitignore diff --git a/data/services/build.gradle.kts b/data/services/build.gradle.kts new file mode 100644 index 00000000..f174322d --- /dev/null +++ b/data/services/build.gradle.kts @@ -0,0 +1,23 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) + alias(libs.plugins.kotlinSerialization) +} + +android { + namespace = "com.twofasapp.data.services" +} + +dependencies { + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(project(":core:storage")) + implementation(project(":core:otp")) + + implementation(project(":parsers")) + + implementation(libs.bundles.room) + implementation(libs.kotlinCoroutines) + implementation(libs.kotlinSerialization) + implementation(libs.timber) +} \ No newline at end of file diff --git a/data/services/src/main/AndroidManifest.xml b/data/services/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/data/services/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/GroupsRepository.kt b/data/services/src/main/java/com/twofasapp/data/services/GroupsRepository.kt new file mode 100644 index 00000000..4681c2fc --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/GroupsRepository.kt @@ -0,0 +1,8 @@ +package com.twofasapp.data.services + +import com.twofasapp.data.services.domain.Group +import kotlinx.coroutines.flow.Flow + +interface GroupsRepository { + fun observeGroups(): Flow> +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/GroupsRepositoryImpl.kt b/data/services/src/main/java/com/twofasapp/data/services/GroupsRepositoryImpl.kt new file mode 100644 index 00000000..fd6fb7bd --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/GroupsRepositoryImpl.kt @@ -0,0 +1,11 @@ +package com.twofasapp.data.services + +import com.twofasapp.data.services.domain.Group +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +internal class GroupsRepositoryImpl : GroupsRepository { + override fun observeGroups(): Flow> { + return flow { } + } +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/ServicesRepository.kt b/data/services/src/main/java/com/twofasapp/data/services/ServicesRepository.kt new file mode 100644 index 00000000..278c97cc --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/ServicesRepository.kt @@ -0,0 +1,16 @@ +package com.twofasapp.data.services + +import com.twofasapp.data.services.domain.Service +import kotlinx.coroutines.flow.Flow + +interface ServicesRepository { + fun observeServices(): Flow> + fun observeServicesTicker(): Flow> + fun observeDeletedServices(): Flow> + fun observeService(id: Long): Flow + suspend fun getServices(): List + suspend fun getService(id: Long): Service + suspend fun deleteService(id: Long) + suspend fun trashService(id: Long) + suspend fun restoreService(id: Long) +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/ServicesRepositoryImpl.kt b/data/services/src/main/java/com/twofasapp/data/services/ServicesRepositoryImpl.kt new file mode 100644 index 00000000..bd70f4a0 --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/ServicesRepositoryImpl.kt @@ -0,0 +1,79 @@ +package com.twofasapp.data.services + +import com.twofasapp.common.coroutines.Dispatchers +import com.twofasapp.common.ktx.tickerFlow +import com.twofasapp.data.services.domain.Service +import com.twofasapp.data.services.local.ServicesLocalSource +import com.twofasapp.data.services.otp.ServiceCodeGenerator +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +internal class ServicesRepositoryImpl( + private val dispatchers: Dispatchers, + private val codeGenerator: ServiceCodeGenerator, + private val localServices: ServicesLocalSource, +) : ServicesRepository { + + override fun observeServices(): Flow> { + return localServices.observeServices() + } + + override fun observeServicesTicker(): Flow> { + return combine( + tickerFlow(1000L), + observeServices(), + ) { a, b -> b } + .map { services -> + services.map { codeGenerator.generate(it) } + } + } + + override fun observeDeletedServices(): Flow> { + return localServices.observeDeletedServices() + } + + override fun observeService(id: Long): Flow { + return localServices.observeService(id) + } + + override suspend fun getServices(): List { + return withContext(dispatchers.io) { + localServices.getServices() + } + } + + override suspend fun getService(id: Long): Service { + return withContext(dispatchers.io) { + localServices.getService(id) + + } + } + + override suspend fun deleteService(id: Long) { + withContext(dispatchers.io) { + localServices.deleteService(id) + } + } + + override suspend fun trashService(id: Long) { + withContext(dispatchers.io) { + localServices.updateService( + localServices.getService(id).copy( + // TODO: see TrashService.kt + ) + ) + } + } + + override suspend fun restoreService(id: Long) { + withContext(dispatchers.io) { + localServices.updateService( + localServices.getService(id).copy( + // TODO: see RestoreService.kt + ) + ) + } + } +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/di/DataServicesModule.kt b/data/services/src/main/java/com/twofasapp/data/services/di/DataServicesModule.kt new file mode 100644 index 00000000..d470ea1d --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/di/DataServicesModule.kt @@ -0,0 +1,22 @@ +package com.twofasapp.data.services.di + +import com.twofasapp.data.services.GroupsRepository +import com.twofasapp.data.services.GroupsRepositoryImpl +import com.twofasapp.data.services.ServicesRepository +import com.twofasapp.data.services.ServicesRepositoryImpl +import com.twofasapp.data.services.local.ServicesLocalSource +import com.twofasapp.data.services.otp.ServiceCodeGenerator +import com.twofasapp.di.KoinModule +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +class DataServicesModule : KoinModule { + override fun provide() = module { + singleOf(::ServiceCodeGenerator) + singleOf(::ServicesLocalSource) + singleOf(::ServicesRepositoryImpl) { bind() } + + singleOf(::GroupsRepositoryImpl) { bind() } + } +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/domain/Group.kt b/data/services/src/main/java/com/twofasapp/data/services/domain/Group.kt new file mode 100644 index 00000000..7cc6bc74 --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/domain/Group.kt @@ -0,0 +1,9 @@ +package com.twofasapp.data.services.domain + +data class Group( + val id: String, + val name: String, + val isExpanded: Boolean = true, + val updatedAt: Long = 0, +// val backupSyncStatus: BackupSyncStatus? = BackupSyncStatus.NOT_SYNCED, +) diff --git a/data/services/src/main/java/com/twofasapp/data/services/domain/Service.kt b/data/services/src/main/java/com/twofasapp/data/services/domain/Service.kt new file mode 100644 index 00000000..d502f15c --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/domain/Service.kt @@ -0,0 +1,95 @@ +package com.twofasapp.data.services.domain + +data class Service( + val id: Long, + val secret: String, + val code: Code? = null, + val name: String, + val info: String?, + val authType: AuthType, + val period: Int?, + val digits: Int?, + val algorithm: Algorithm?, + + val imageType: ImageType = ImageType.IconCollection, + val iconCollectionId: String, + val iconLight: String, + val iconDark: String, + val labelText: String? = null, + val labelColor: Tint? = null, + val badgeColor: Tint?, + + val isDeleted: Boolean = false, + + +// val otp: Otp = Otp(), + +//// +// val groupId: String? = null, +// val assignedDomains: List = emptyList(), +// val isDeleted: Boolean = false, +//// val backupSyncStatus: BackupSyncStatus = BackupSyncStatus.NOT_SYNCED, +// var updatedAt: Long = 0, +// val serviceTypeId: String?, +// val source: Source, +// val tags: List = emptyList(), +) { + +// + + data class Code( + val current: String, + val next: String, + val timer: Int, + val progress: Float, + ) + + enum class AuthType { TOTP, HOTP } + enum class Algorithm { SHA1, SHA224, SHA256, SHA384, SHA512 } + enum class ImageType { IconCollection, Label } + enum class Source { Link, Manual } + enum class Tint { + Default, + LightBlue, + Indigo, + Purple, + Turquoise, + Green, + Red, + Orange, + Yellow; + } +// companion object { +// const val DefaultPeriod = 30 +// const val DefaultDigits = 6 +// const val DefaultHotpCounter = 1 +// val DefaultAlgorithm = OtpData.Algorithm.SHA1 +// val DefaultAuthType = AuthType.TOTP +// val DefaultSource = Source.Manual +// +//// fun createDefault() = Service( +//// id = 0, +//// name = "", +//// secret = "", +////// iconCollectionId = ServiceIcons.defaultCollectionId, +//// serviceTypeId = null, +//// source = Source.Manual, +//// ) +// } + +// data class Otp( +// val link: String? = null, +// val label: String = "", +// val account: String = "", +// val issuer: String? = null, +// val digits: Int = DefaultDigits, +// val period: Int = DefaultPeriod, +// val hotpCounter: Int? = null, +// val algorithm: Algorithm = DefaultAlgorithm, +// ) + + + + + +} diff --git a/data/services/src/main/java/com/twofasapp/data/services/local/GroupsLocalSource.kt b/data/services/src/main/java/com/twofasapp/data/services/local/GroupsLocalSource.kt new file mode 100644 index 00000000..943b24b3 --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/local/GroupsLocalSource.kt @@ -0,0 +1,11 @@ +package com.twofasapp.data.services.local + +import com.twofasapp.storage.PlainPreferences + +internal class GroupsLocalSource( + private val preferences: PlainPreferences, +) { + companion object { + private const val KeyGroups = "groups" + } +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/local/ServiceDao.kt b/data/services/src/main/java/com/twofasapp/data/services/local/ServiceDao.kt new file mode 100644 index 00000000..fc9e861f --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/local/ServiceDao.kt @@ -0,0 +1,64 @@ +package com.twofasapp.data.services.local + +import androidx.room.* +import com.twofasapp.data.services.local.model.ServiceEntity +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Single +import kotlinx.coroutines.flow.Flow + +@Dao +interface ServiceDao { + + @Query("SELECT * FROM local_services") + suspend fun select(): List + + @Query("SELECT * FROM local_services") + fun observe(): Flow> + + @Query("SELECT * FROM local_services WHERE id=:id") + suspend fun select(id: Long): ServiceEntity + + @Query("SELECT * FROM local_services WHERE id=:id") + fun observe(id: Long): Flow + + @Query("DELETE FROM local_services WHERE id == :id") + suspend fun delete(id: Long) + + @Update + suspend fun update(entity: ServiceEntity) + + // LLegacy + @Query("SELECT * FROM local_services") + fun legacySelect(): Single> + + @Query("SELECT * FROM local_services") + suspend fun legacySelectAll(): List + + @Query("SELECT * FROM local_services") + fun legacySelectFlow(): Flow> + + @Query("SELECT * FROM local_services") + fun legacyObserve(): Flowable> + + @Query("SELECT * FROM local_services WHERE id=:serviceId") + fun legacyObserve(serviceId: Long): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun legacyInsert(serviceEntity: ServiceEntity): Single + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun legacyInsertSuspend(serviceEntity: ServiceEntity): Long + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun legacyUpdate(vararg serviceEntity: ServiceEntity): Completable + + @Update(onConflict = OnConflictStrategy.REPLACE) + suspend fun legacyUpdateSuspend(vararg serviceEntity: ServiceEntity) + + @Query("DELETE FROM local_services WHERE id IN (:ids)") + fun legacyDeleteById(ids: List): Completable + + @Query("DELETE FROM local_services WHERE id == :id") + suspend fun legacyDeleteById(id: Long) +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/local/ServicesLocalSource.kt b/data/services/src/main/java/com/twofasapp/data/services/local/ServicesLocalSource.kt new file mode 100644 index 00000000..fc60ab62 --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/local/ServicesLocalSource.kt @@ -0,0 +1,193 @@ +package com.twofasapp.data.services.local + +import com.twofasapp.data.services.domain.Service +import com.twofasapp.data.services.mapper.asDomain +import com.twofasapp.data.services.mapper.asEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import timber.log.Timber + +internal class ServicesLocalSource( + private val dao: ServiceDao +) { + private fun log(msg: String) { + Timber.tag("ServicesDao").i(msg) + } + + fun observeServices(): Flow> { + return dao.observe().map { list -> + list.filter { it.isDeleted != true }.map { it.asDomain() } + } + } + + fun observeDeletedServices(): Flow> { + return dao.observe().map { list -> + list.filter { it.isDeleted == true }.map { it.asDomain() } + } + } + + suspend fun getServices(): List { + return dao.select().map { it.asDomain() } + } + + fun observeService(id: Long): Flow { + return dao.observe(id).map { it.asDomain() } + } + + suspend fun getService(id: Long): Service { + return dao.select(id).asDomain() + } + + suspend fun deleteService(id: Long) { + log("Delete service $id") + dao.delete(id) + } + + suspend fun updateService(service: Service) { + dao.update(service.asEntity()) + } +// +// fun select(): Single> { +// return dao.legacySelect() +// .map { list -> +// list.map { local -> +// ServiceDto( +// id = local.id, +// name = local.name, +// secret = local.secret, +// authType = local.authType?.let { ServiceDto.AuthType.valueOf(it) } ?: ServiceDto.AuthType.TOTP, +// otpLabel = local.otpLabel, +// otpAccount = local.otpAccount, +// otpIssuer = local.otpIssuer, +// otpDigits = local.otpDigits, +// otpPeriod = local.otpPeriod, +// otpAlgorithm = local.otpAlgorithm, +// hotpCounter = local.hotpCounter, +// backupSyncStatus = BackupSyncStatus.valueOf(local.backupSyncStatus), +// updatedAt = local.updatedAt, +// badge = local.badgeColor?.let { ServiceDto.Badge(Tint.valueOf(it)) }, +// selectedImageType = local.selectedImageType?.let { +// when (it) { +// "Brand" -> ServiceDto.ImageType.IconCollection +// "Label" -> ServiceDto.ImageType.Label +// else -> ServiceDto.ImageType.IconCollection +// } +// } ?: ServiceDto.ImageType.IconCollection, +// labelText = local.labelText, +// labelBackgroundColor = local.labelBackgroundColor?.let { color -> Tint.valueOf(color) }, +// iconCollectionId = local.iconCollectionId ?: ServiceIcons.defaultCollectionId, +// groupId = local.groupId, +// isDeleted = local.isDeleted, +// assignedDomains = local.assignedDomains.orEmpty(), +// serviceTypeId = local.serviceTypeId, +// source = local.source?.let { ServiceDto.Source.valueOf(it) } ?: ServiceDto.Source.Manual +// ) +// } +// } +// } +// +// fun observe(): Flowable> { +// return dao.legacyObserve() +// .map { list -> +// list.map { local -> +// ServiceDto( +// id = local.id, +// name = local.name, +// secret = local.secret, +// authType = local.authType?.let { ServiceDto.AuthType.valueOf(it) } ?: ServiceDto.AuthType.TOTP, +// otpLabel = local.otpLabel, +// otpAccount = local.otpAccount, +// otpIssuer = local.otpIssuer, +// otpDigits = local.otpDigits, +// otpPeriod = local.otpPeriod, +// otpAlgorithm = local.otpAlgorithm, +// hotpCounter = local.hotpCounter, +// backupSyncStatus = BackupSyncStatus.valueOf(local.backupSyncStatus), +// updatedAt = local.updatedAt, +// badge = local.badgeColor?.let { ServiceDto.Badge(Tint.valueOf(it)) }, +// selectedImageType = local.selectedImageType?.let { +// when (it) { +// "Brand" -> ServiceDto.ImageType.IconCollection +// "Label" -> ServiceDto.ImageType.Label +// else -> ServiceDto.ImageType.IconCollection +// } +// } ?: ServiceDto.ImageType.IconCollection, +// labelText = local.labelText, +// labelBackgroundColor = local.labelBackgroundColor?.let { color -> Tint.valueOf(color) }, +// iconCollectionId = local.iconCollectionId ?: ServiceIcons.defaultCollectionId, +// groupId = local.groupId, +// isDeleted = local.isDeleted, +// assignedDomains = local.assignedDomains.orEmpty(), +// serviceTypeId = local.serviceTypeId, +// source = local.source?.let { ServiceDto.Source.valueOf(it) } ?: ServiceDto.Source.Manual +// ) +// } +// } +// } +// +// fun insertService(service: ServiceDto): Single { +// Timber.d("InsertService: $service") +// return dao.legacyInsert( +// ServiceEntity( +// id = 0, +// name = service.name, +// secret = service.secret.removeWhiteCharacters(), +// serviceTypeId = service.serviceTypeId, +// iconCollectionId = service.iconCollectionId, +// source = service.source.name, +// otpLink = service.otpLink, +// otpLabel = service.otpLabel, +// otpAccount = service.otpAccount, +// otpIssuer = service.otpIssuer, +// otpDigits = service.getDigits(), +// otpPeriod = service.getPeriod(), +// otpAlgorithm = service.getAlgorithm(), +// backupSyncStatus = service.backupSyncStatus.name, +// updatedAt = service.updatedAt, +// badgeColor = service.badge?.color?.name, +// selectedImageType = service.selectedImageType.name, +// labelText = service.labelText, +// labelBackgroundColor = service.labelBackgroundColor?.name, +// groupId = service.groupId, +// isDeleted = service.isDeleted, +// authType = service.authType.name, +// hotpCounter = service.hotpCounter, +// assignedDomains = service.assignedDomains +// ) +// ) +// } +// +// fun updateService(vararg services: ServiceDto): Completable { +// Timber.d("UpdateServices: ${services.toList()}") +// return dao.legacyUpdate( +// *services.map { +// ServiceEntity( +// id = it.id, +// name = it.name, +// secret = it.secret, +// serviceTypeId = it.serviceTypeId, +// iconCollectionId = it.iconCollectionId, +// source = it.source.name, +// otpLink = it.otpLink, +// otpLabel = it.otpLabel, +// otpAccount = it.otpAccount, +// otpIssuer = it.otpIssuer, +// otpDigits = it.getDigits(), +// otpPeriod = it.getPeriod(), +// otpAlgorithm = it.getAlgorithm(), +// backupSyncStatus = it.backupSyncStatus.name, +// updatedAt = it.updatedAt, +// badgeColor = it.badge?.color?.name, +// selectedImageType = it.selectedImageType.name, +// labelText = it.labelText, +// labelBackgroundColor = it.labelBackgroundColor?.name, +// groupId = it.groupId, +// isDeleted = it.isDeleted, +// authType = it.authType.name, +// hotpCounter = it.hotpCounter, +// assignedDomains = it.assignedDomains, +// ) +// }.toTypedArray() +// ) +// } +} \ No newline at end of file diff --git a/persistence/src/main/java/com/twofasapp/persistence/model/ServiceEntity.kt b/data/services/src/main/java/com/twofasapp/data/services/local/model/ServiceEntity.kt similarity index 97% rename from persistence/src/main/java/com/twofasapp/persistence/model/ServiceEntity.kt rename to data/services/src/main/java/com/twofasapp/data/services/local/model/ServiceEntity.kt index 4bbcea61..d1e7dddf 100644 --- a/persistence/src/main/java/com/twofasapp/persistence/model/ServiceEntity.kt +++ b/data/services/src/main/java/com/twofasapp/data/services/local/model/ServiceEntity.kt @@ -1,4 +1,4 @@ -package com.twofasapp.persistence.model +package com.twofasapp.data.services.local.model import androidx.room.ColumnInfo import androidx.room.Entity diff --git a/data/services/src/main/java/com/twofasapp/data/services/mapper/ServiceMapper.kt b/data/services/src/main/java/com/twofasapp/data/services/mapper/ServiceMapper.kt new file mode 100644 index 00000000..77ba13e0 --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/mapper/ServiceMapper.kt @@ -0,0 +1,64 @@ +package com.twofasapp.data.services.mapper + +import com.twofasapp.data.services.domain.Service +import com.twofasapp.data.services.local.model.ServiceEntity +import com.twofasapp.parsers.ServiceIcons + +internal fun ServiceEntity.asDomain(): Service { + val iconCollectionId = iconCollectionId ?: ServiceIcons.defaultCollectionId + val iconLight = ServiceIcons.getIcon(iconCollectionId, false) + val iconDark = ServiceIcons.getIcon(iconCollectionId, true) + + return Service( + id = id, + secret = secret, + name = name, + info = otpAccount, + authType = authType?.let { Service.AuthType.valueOf(it) } ?: Service.AuthType.TOTP, + period = otpPeriod, + digits = otpDigits, + algorithm = otpAlgorithm?.let { Service.Algorithm.valueOf(it) }, + imageType = selectedImageType?.let { + when (it) { + "Brand" -> Service.ImageType.IconCollection + "Label" -> Service.ImageType.Label + else -> Service.ImageType.IconCollection + } + } ?: Service.ImageType.IconCollection, + iconCollectionId = iconCollectionId, + iconLight = iconLight, + iconDark = iconDark, + labelText = labelText, + labelColor = labelBackgroundColor?.let { color -> Service.Tint.valueOf(color) }, + badgeColor = badgeColor?.let { Service.Tint.valueOf(it) } + ) +} + +internal fun Service.asEntity(): ServiceEntity { + return ServiceEntity( + id = 7479L, + name = "Jodie", + secret = "Rudi", + serviceTypeId = null, + iconCollectionId = null, + source = null, + otpLink = null, + otpLabel = null, + otpAccount = null, + otpIssuer = null, + otpDigits = null, + otpPeriod = null, + otpAlgorithm = null, + backupSyncStatus = "Joycelyn", + updatedAt = 663L, + badgeColor = null, + selectedImageType = null, + labelText = null, + labelBackgroundColor = null, + groupId = null, + isDeleted = null, + authType = null, + hotpCounter = null, + assignedDomains = listOf() + ) +} \ No newline at end of file diff --git a/data/services/src/main/java/com/twofasapp/data/services/otp/ServiceCodeGenerator.kt b/data/services/src/main/java/com/twofasapp/data/services/otp/ServiceCodeGenerator.kt new file mode 100644 index 00000000..f4daa7f1 --- /dev/null +++ b/data/services/src/main/java/com/twofasapp/data/services/otp/ServiceCodeGenerator.kt @@ -0,0 +1,59 @@ +package com.twofasapp.data.services.otp + +import com.twofasapp.common.time.TimeProvider +import com.twofasapp.data.services.domain.Service +import com.twofasapp.otp.OtpAuthenticator +import com.twofasapp.otp.OtpData +import java.time.Instant + +class ServiceCodeGenerator( + private val timeProvider: TimeProvider, +) { + private val authenticator = OtpAuthenticator() + + fun generate(service: Service): Service { + val period = service.period ?: 30 + val digits = service.digits ?: 6 + val algorithm = when (service.algorithm) { + Service.Algorithm.SHA1 -> OtpData.Algorithm.SHA1 + Service.Algorithm.SHA224 -> OtpData.Algorithm.SHA224 + Service.Algorithm.SHA256 -> OtpData.Algorithm.SHA256 + Service.Algorithm.SHA384 -> OtpData.Algorithm.SHA384 + Service.Algorithm.SHA512 -> OtpData.Algorithm.SHA512 + null -> OtpData.Algorithm.SHA1 + } + + // TODO: Handle HOTP + val currentCounter = timeProvider.systemCurrentTime() / period.toMillis() + val nextCounter = (timeProvider.systemCurrentTime() + period.toMillis()) / period.toMillis() + + val otpData = OtpData( + counter = currentCounter, + secret = service.secret, + digits = digits, + period = period, + algorithm = algorithm, + ) + + val timer = calculateTimer(period) + + return service.copy( + code = Service.Code( + current = authenticator.generateOtpCode(otpData), + next = authenticator.generateOtpCode(otpData.copy(counter = nextCounter)), + timer = timer, + progress = timer / period.toFloat() + ) + ) + } + + private fun calculateTimer(period: Int): Int { +// return 30 - (Instant.now().epochSecond + timeProvider.realTimeDelta() / 1000) % 30 + return (period - (Instant.now().epochSecond) % period).toInt() + + } + + private fun Int.toMillis(): Long { + return this * 1000L + } +} \ No newline at end of file diff --git a/data/session/build.gradle.kts b/data/session/build.gradle.kts index 8e26de49..1dbeed4f 100644 --- a/data/session/build.gradle.kts +++ b/data/session/build.gradle.kts @@ -9,7 +9,7 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":core:storage")) implementation(libs.kotlinCoroutines) diff --git a/data/session/src/main/java/com/twofasapp/data/session/SessionRepository.kt b/data/session/src/main/java/com/twofasapp/data/session/SessionRepository.kt index 7a4fe394..e51e6b7a 100644 --- a/data/session/src/main/java/com/twofasapp/data/session/SessionRepository.kt +++ b/data/session/src/main/java/com/twofasapp/data/session/SessionRepository.kt @@ -3,4 +3,5 @@ package com.twofasapp.data.session interface SessionRepository { suspend fun isOnboardingDisplayed(): Boolean suspend fun setOnboardingDisplayed(isDisplayed: Boolean) + suspend fun setRateAppDisplayed(isDisplayed: Boolean) } \ No newline at end of file diff --git a/data/session/src/main/java/com/twofasapp/data/session/SessionRepositoryImpl.kt b/data/session/src/main/java/com/twofasapp/data/session/SessionRepositoryImpl.kt index e622a6d7..92ea1d5b 100644 --- a/data/session/src/main/java/com/twofasapp/data/session/SessionRepositoryImpl.kt +++ b/data/session/src/main/java/com/twofasapp/data/session/SessionRepositoryImpl.kt @@ -2,15 +2,19 @@ package com.twofasapp.data.session import com.twofasapp.data.session.local.SessionLocalSource -class SessionRepositoryImpl( - private val localSource: SessionLocalSource, +internal class SessionRepositoryImpl( + private val local: SessionLocalSource, ) : SessionRepository { override suspend fun isOnboardingDisplayed(): Boolean { - return localSource.isOnboardingDisplayed() + return local.isOnboardingDisplayed() } override suspend fun setOnboardingDisplayed(isDisplayed: Boolean) { - localSource.setOnboardingDisplayed(isDisplayed) + local.setOnboardingDisplayed(isDisplayed) + } + + override suspend fun setRateAppDisplayed(isDisplayed: Boolean) { + } } \ No newline at end of file diff --git a/data/session/src/main/java/com/twofasapp/data/session/SettingsRepository.kt b/data/session/src/main/java/com/twofasapp/data/session/SettingsRepository.kt new file mode 100644 index 00000000..ce4be7b9 --- /dev/null +++ b/data/session/src/main/java/com/twofasapp/data/session/SettingsRepository.kt @@ -0,0 +1,9 @@ +package com.twofasapp.data.session + +import com.twofasapp.data.session.domain.AppSettings +import kotlinx.coroutines.flow.Flow + +interface SettingsRepository { + fun observeAppSettings(): Flow + suspend fun setShowNextToken(showNextToken: Boolean) +} \ No newline at end of file diff --git a/data/session/src/main/java/com/twofasapp/data/session/SettingsRepositoryImpl.kt b/data/session/src/main/java/com/twofasapp/data/session/SettingsRepositoryImpl.kt new file mode 100644 index 00000000..d2944cc1 --- /dev/null +++ b/data/session/src/main/java/com/twofasapp/data/session/SettingsRepositoryImpl.kt @@ -0,0 +1,18 @@ +package com.twofasapp.data.session + +import com.twofasapp.data.session.domain.AppSettings +import com.twofasapp.data.session.local.SettingsLocalSource +import kotlinx.coroutines.flow.Flow + +internal class SettingsRepositoryImpl( + private val local: SettingsLocalSource, +) : SettingsRepository { + + override fun observeAppSettings(): Flow { + return local.observeAppSettings() + } + + override suspend fun setShowNextToken(showNextToken: Boolean) { + local.setShowNextToken(showNextToken) + } +} \ No newline at end of file diff --git a/data/session/src/main/java/com/twofasapp/data/session/di/DataSessionModule.kt b/data/session/src/main/java/com/twofasapp/data/session/di/DataSessionModule.kt index f7822566..05c8f96b 100644 --- a/data/session/src/main/java/com/twofasapp/data/session/di/DataSessionModule.kt +++ b/data/session/src/main/java/com/twofasapp/data/session/di/DataSessionModule.kt @@ -2,8 +2,10 @@ package com.twofasapp.data.session.di import com.twofasapp.data.session.SessionRepository import com.twofasapp.data.session.SessionRepositoryImpl +import com.twofasapp.data.session.SettingsRepository +import com.twofasapp.data.session.SettingsRepositoryImpl import com.twofasapp.data.session.local.SessionLocalSource -import com.twofasapp.data.session.local.SessionLocalSourceImpl +import com.twofasapp.data.session.local.SettingsLocalSource import com.twofasapp.di.KoinModule import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf @@ -11,7 +13,10 @@ import org.koin.dsl.module class DataSessionModule : KoinModule { override fun provide() = module { - singleOf(::SessionLocalSourceImpl) { bind() } + singleOf(::SessionLocalSource) singleOf(::SessionRepositoryImpl) { bind() } + + singleOf(::SettingsLocalSource) + singleOf(::SettingsRepositoryImpl) { bind() } } } \ No newline at end of file diff --git a/data/session/src/main/java/com/twofasapp/data/session/domain/AppSettings.kt b/data/session/src/main/java/com/twofasapp/data/session/domain/AppSettings.kt new file mode 100644 index 00000000..cc4cf8d8 --- /dev/null +++ b/data/session/src/main/java/com/twofasapp/data/session/domain/AppSettings.kt @@ -0,0 +1,5 @@ +package com.twofasapp.data.session.domain + +data class AppSettings( + val showNextToken: Boolean, +) diff --git a/data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSource.kt b/data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSource.kt index d6774732..0b310163 100644 --- a/data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSource.kt +++ b/data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSource.kt @@ -1,6 +1,18 @@ package com.twofasapp.data.session.local -interface SessionLocalSource { - suspend fun isOnboardingDisplayed(): Boolean - suspend fun setOnboardingDisplayed(isDisplayed: Boolean) +import com.twofasapp.storage.PlainPreferences + +internal class SessionLocalSource(private val preferences: PlainPreferences) { + + companion object { + private const val KeyShowOnboardWarning = "showOnboardWarning" + } + + suspend fun isOnboardingDisplayed(): Boolean { + return preferences.getBoolean(KeyShowOnboardWarning)?.not() ?: false + } + + suspend fun setOnboardingDisplayed(isDisplayed: Boolean) { + preferences.putBoolean(KeyShowOnboardWarning, isDisplayed.not()) + } } \ No newline at end of file diff --git a/data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSourceImpl.kt b/data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSourceImpl.kt deleted file mode 100644 index 87dd6a0c..00000000 --- a/data/session/src/main/java/com/twofasapp/data/session/local/SessionLocalSourceImpl.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.twofasapp.data.session.local - -import com.twofasapp.storage.PlainPreferences - -class SessionLocalSourceImpl(private val preferences: PlainPreferences) : SessionLocalSource { - - companion object { - private const val KeyShowOnboardWarning = "showOnboardWarning" - } - - override suspend fun isOnboardingDisplayed(): Boolean { - return preferences.getBoolean(KeyShowOnboardWarning)?.not() ?: false - } - - override suspend fun setOnboardingDisplayed(isDisplayed: Boolean) { - preferences.putBoolean(KeyShowOnboardWarning, isDisplayed.not()) - } -} \ No newline at end of file diff --git a/data/session/src/main/java/com/twofasapp/data/session/local/SettingsLocalSource.kt b/data/session/src/main/java/com/twofasapp/data/session/local/SettingsLocalSource.kt new file mode 100644 index 00000000..6a7dff8c --- /dev/null +++ b/data/session/src/main/java/com/twofasapp/data/session/local/SettingsLocalSource.kt @@ -0,0 +1,31 @@ +package com.twofasapp.data.session.local + +import com.twofasapp.data.session.domain.AppSettings +import com.twofasapp.storage.PlainPreferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal class SettingsLocalSource(private val preferences: PlainPreferences) { + + companion object { + private const val KeyShowNextToken = "showNextToken" + } + + private val appSettingsFlow: MutableStateFlow by lazy { + MutableStateFlow( + AppSettings( + showNextToken = preferences.getBoolean(KeyShowNextToken) ?: false + ) + ) + } + + fun observeAppSettings(): Flow { + return appSettingsFlow + } + + suspend fun setShowNextToken(showNextToken: Boolean) { + appSettingsFlow.update { it.copy(showNextToken = showNextToken) } + preferences.putBoolean(KeyShowNextToken, showNextToken) + } +} \ No newline at end of file diff --git a/design/build.gradle.kts b/design/build.gradle.kts index 801b8f05..82937382 100644 --- a/design/build.gradle.kts +++ b/design/build.gradle.kts @@ -9,12 +9,14 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":prefs")) implementation(project(":extensions")) implementation(project(":resources")) implementation(project(":parsers")) + implementation(project(":core:designsystem")) + implementation(libs.bundles.fastAdapter) implementation(libs.bundles.materialDialogs) implementation(libs.bundles.appCompat) diff --git a/design/src/main/java/com/twofasapp/design/compose/SimpleEntry.kt b/design/src/main/java/com/twofasapp/design/compose/SimpleEntry.kt index 3a0bc778..4e8f8070 100644 --- a/design/src/main/java/com/twofasapp/design/compose/SimpleEntry.kt +++ b/design/src/main/java/com/twofasapp/design/compose/SimpleEntry.kt @@ -6,10 +6,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -22,10 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import com.twofasapp.design.R -import com.twofasapp.design.theme.icon -import com.twofasapp.design.theme.textPrimary -import com.twofasapp.design.theme.textSecondary +import com.twofasapp.designsystem.TwTheme enum class SubtitleGravity { BOTTOM, END } @@ -42,7 +39,7 @@ fun SimpleEntry( iconEndTint: Color = Color.Unspecified, isEnabled: Boolean = true, iconEndClick: (() -> Unit)? = null, - titleColor: Color = MaterialTheme.colors.textPrimary, + titleColor: Color = TwTheme.color.onSurfacePrimary, modifier: Modifier = Modifier, click: (() -> Unit)? = null, ) { @@ -75,7 +72,7 @@ fun SimpleEntry( Icon( painter = icon ?: painterResource(com.twofasapp.resources.R.drawable.ic_placeholder), contentDescription = null, - tint = if (iconTint != Color.Unspecified) iconTint else MaterialTheme.colors.primary, + tint = if (iconTint != Color.Unspecified) iconTint else TwTheme.color.primary, modifier = Modifier .size(if (iconVisibleWhenNotSet) 24.dp else 0.dp) .alpha(if (icon == null) 0f else alpha) @@ -107,7 +104,7 @@ fun SimpleEntry( if (subtitle.isNotEmpty() && subtitleGravity == SubtitleGravity.BOTTOM) { Text( text = subtitle, - style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = MaterialTheme.colors.textSecondary), + style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = TwTheme.color.onSurfaceSecondary), modifier = Modifier .alpha(alpha) .constrainAs(subtitleRef) { @@ -123,7 +120,7 @@ fun SimpleEntry( if (subtitle.isNotEmpty() && subtitleGravity == SubtitleGravity.END) { Text( text = subtitle, - style = MaterialTheme.typography.body2.copy(fontSize = 16.sp, color = MaterialTheme.colors.textSecondary), + style = MaterialTheme.typography.body2.copy(fontSize = 16.sp, color = TwTheme.color.onSurfaceSecondary), modifier = Modifier .alpha(alpha) .constrainAs(subtitleEndRef) { @@ -154,7 +151,7 @@ fun SimpleEntry( Icon( painter = iconEnd ?: painterResource(com.twofasapp.resources.R.drawable.ic_placeholder), contentDescription = null, - tint = if (iconEndTint != Color.Unspecified) iconEndTint else MaterialTheme.colors.icon, + tint = if (iconEndTint != Color.Unspecified) iconEndTint else TwTheme.color.iconTint, ) } diff --git a/design/src/main/java/com/twofasapp/design/compose/SwitchEntry.kt b/design/src/main/java/com/twofasapp/design/compose/SwitchEntry.kt index 3e932850..9b4f2128 100644 --- a/design/src/main/java/com/twofasapp/design/compose/SwitchEntry.kt +++ b/design/src/main/java/com/twofasapp/design/compose/SwitchEntry.kt @@ -5,7 +5,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.* +import androidx.compose.material3.* +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -18,11 +19,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import com.twofasapp.design.R import com.twofasapp.design.theme.radioColors import com.twofasapp.design.theme.switchColors -import com.twofasapp.design.theme.textPrimary -import com.twofasapp.design.theme.textSecondary +import com.twofasapp.designsystem.TwTheme enum class SwitchEntryType { Switch, Radio } @@ -54,7 +53,7 @@ fun SwitchEntry( Icon( painter = icon ?: painterResource(com.twofasapp.resources.R.drawable.ic_placeholder), contentDescription = null, - tint = if (iconTint != Color.Unspecified) iconTint else MaterialTheme.colors.primary, + tint = if (iconTint != Color.Unspecified) iconTint else TwTheme.color.primary, modifier = Modifier .size(if (iconVisible) 24.dp else 0.dp) .alpha(if (icon == null) 0f else alpha) @@ -67,7 +66,7 @@ fun SwitchEntry( Text( text = title, - style = MaterialTheme.typography.body2.copy(fontSize = 17.sp, color = MaterialTheme.colors.textPrimary), + style = MaterialTheme.typography.body2.copy(fontSize = 17.sp, color =TwTheme.color.onSurfacePrimary), modifier = Modifier .alpha(alpha) .constrainAs(titleRef) { @@ -85,7 +84,7 @@ fun SwitchEntry( if (subtitle.isNotEmpty()) { Text( text = subtitle, - style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = MaterialTheme.colors.textSecondary), + style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = TwTheme.color.onSurfaceSecondary), modifier = Modifier .alpha(alpha) .constrainAs(subtitleRef) { diff --git a/design/src/main/java/com/twofasapp/design/compose/Toolbar.kt b/design/src/main/java/com/twofasapp/design/compose/Toolbar.kt index b4ba50bd..cc4e7138 100644 --- a/design/src/main/java/com/twofasapp/design/compose/Toolbar.kt +++ b/design/src/main/java/com/twofasapp/design/compose/Toolbar.kt @@ -1,7 +1,11 @@ package com.twofasapp.design.compose import androidx.compose.foundation.layout.RowScope -import androidx.compose.material.* +import androidx.compose.material.AppBarDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable @@ -9,30 +13,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp -import com.twofasapp.design.theme.textPrimary -import com.twofasapp.design.theme.toolbar -import com.twofasapp.design.theme.toolbarContent +import com.twofasapp.designsystem.TwTheme @Composable fun Toolbar( title: String, - backgroundColor: Color = MaterialTheme.colors.toolbar, + backgroundColor: Color = TwTheme.color.background, elevation: Dp = AppBarDefaults.TopAppBarElevation, modifier: Modifier = Modifier, actions: @Composable RowScope.() -> Unit = {}, navigationClick: () -> Unit ) { TopAppBar( - title = { Text(text = title, color = MaterialTheme.colors.textPrimary) }, + title = { Text(text = title, color = TwTheme.color.onSurfacePrimary) }, navigationIcon = { IconButton(onClick = { navigationClick.invoke() }) { - Icon(Icons.Filled.ArrowBack, null, tint = MaterialTheme.colors.toolbarContent) + Icon(Icons.Filled.ArrowBack, null, tint = TwTheme.color.onSurfacePrimary) } }, actions = actions, backgroundColor = backgroundColor, - contentColor = MaterialTheme.colors.toolbarContent, + contentColor = TwTheme.color.onSurfacePrimary, elevation = elevation, modifier = modifier ) diff --git a/design/src/main/java/com/twofasapp/design/compose/dialogs/InputDialog.kt b/design/src/main/java/com/twofasapp/design/compose/dialogs/InputDialog.kt index cc7443b9..55bd3308 100644 --- a/design/src/main/java/com/twofasapp/design/compose/dialogs/InputDialog.kt +++ b/design/src/main/java/com/twofasapp/design/compose/dialogs/InputDialog.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp diff --git a/design/src/main/java/com/twofasapp/design/compose/dialogs/ListDialog.kt b/design/src/main/java/com/twofasapp/design/compose/dialogs/ListDialog.kt index c51f0b5d..507d7e3d 100644 --- a/design/src/main/java/com/twofasapp/design/compose/dialogs/ListDialog.kt +++ b/design/src/main/java/com/twofasapp/design/compose/dialogs/ListDialog.kt @@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.MaterialTheme -import androidx.compose.material.RadioButton -import androidx.compose.material.Text +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier diff --git a/design/src/main/java/com/twofasapp/design/theme/Color.kt b/design/src/main/java/com/twofasapp/design/theme/Color.kt index ba055bd5..4dc2c360 100644 --- a/design/src/main/java/com/twofasapp/design/theme/Color.kt +++ b/design/src/main/java/com/twofasapp/design/theme/Color.kt @@ -1,9 +1,13 @@ package com.twofasapp.design.theme -import androidx.compose.material.* -import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.Colors +import androidx.compose.material3.RadioButtonColors +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.SwitchColors +import androidx.compose.material3.SwitchDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import com.twofasapp.designsystem.TwTheme fun Color.Companion.parse(hexString: String): Color = Color(color = android.graphics.Color.parseColor(hexString)) @@ -11,8 +15,8 @@ fun Color.Companion.parse(hexString: String): Color = @Composable fun switchColors(): SwitchColors { return SwitchDefaults.colors( - checkedThumbColor = colors.primary, - checkedTrackColor = colors.primary, + checkedThumbColor = TwTheme.color.primary, + checkedTrackColor = TwTheme.color.primary, uncheckedThumbColor = Color(0xFFECECEC), uncheckedTrackColor = Color(0xFF585858), ) @@ -21,7 +25,7 @@ fun switchColors(): SwitchColors { @Composable fun radioColors(): RadioButtonColors { return RadioButtonDefaults.colors( - selectedColor = colors.primary, + selectedColor = TwTheme.color.primary, unselectedColor = Color(0xFF585858), ) } diff --git a/developer/build.gradle.kts b/developer/build.gradle.kts index 0681de1c..00a4c240 100644 --- a/developer/build.gradle.kts +++ b/developer/build.gradle.kts @@ -10,11 +10,11 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":resources")) implementation(project(":extensions")) implementation(project(":design")) - implementation(project(":environment")) + implementation(project(":prefs")) implementation(project(":featuretoggle")) implementation(project(":push")) diff --git a/environment/build.gradle.kts b/environment/build.gradle.kts deleted file mode 100644 index 4c07c02b..00000000 --- a/environment/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -@Suppress("DSL_SCOPE_VIOLATION") -plugins { - alias(libs.plugins.twofasAndroidLibrary) -} - -android { - namespace = "com.twofasapp.environment" -} - -dependencies { -} \ No newline at end of file diff --git a/environment/src/main/java/com/twofasapp/environment/BuildVariant.kt b/environment/src/main/java/com/twofasapp/environment/BuildVariant.kt deleted file mode 100644 index 5c50d910..00000000 --- a/environment/src/main/java/com/twofasapp/environment/BuildVariant.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.twofasapp.environment - -enum class BuildVariant { - Debug, ReleaseLocal, Release, -} \ No newline at end of file diff --git a/externalimport/src/main/AndroidManifest.xml b/externalimport/src/main/AndroidManifest.xml deleted file mode 100644 index ae4329a3..00000000 --- a/externalimport/src/main/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ExternalImportModule.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ExternalImportModule.kt deleted file mode 100644 index be8c3761..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ExternalImportModule.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.twofasapp.externalimport - -import com.twofasapp.di.KoinModule -import com.twofasapp.externalimport.domain.AegisImporter -import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter -import com.twofasapp.externalimport.domain.RaivoImporter -import com.twofasapp.externalimport.ui.aegis.AegisScreenFactory -import com.twofasapp.externalimport.ui.googleauthenticator.GoogleAuthenticatorScreenFactory -import com.twofasapp.externalimport.ui.main.ExternalImportScreenFactory -import com.twofasapp.externalimport.ui.raivo.RaivoScreenFactory -import com.twofasapp.externalimport.ui.result.ImportResultScreenFactory -import com.twofasapp.externalimport.ui.result.ImportResultViewModel -import com.twofasapp.externalimport.ui.scan.ImportScanScreenFactory -import com.twofasapp.externalimport.ui.scan.ImportScanViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.factoryOf -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -class ExternalImportModule : KoinModule { - - override fun provide() = module { - viewModelOf(::ImportScanViewModel) - viewModelOf(::ImportResultViewModel) - - factoryOf(::GoogleAuthenticatorImporter) - factoryOf(::AegisImporter) - factoryOf(::RaivoImporter) - - singleOf(::ExternalImportScreenFactory) - singleOf(::GoogleAuthenticatorScreenFactory) - singleOf(::AegisScreenFactory) - singleOf(::RaivoScreenFactory) - singleOf(::ImportScanScreenFactory) - singleOf(::ImportResultScreenFactory) - } -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/ExternalImportActivity.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/ExternalImportActivity.kt deleted file mode 100644 index 1837ca1c..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/ExternalImportActivity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.twofasapp.externalimport.ui - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.material.Surface -import com.twofasapp.base.BaseComponentActivity -import com.twofasapp.design.theme.AppThemeLegacy -import com.twofasapp.navigation.ExternalImportRouter -import com.twofasapp.navigation.base.RouterNavHost -import org.koin.androidx.compose.get - -class ExternalImportActivity : BaseComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - AppThemeLegacy { - Surface { - RouterNavHost(router = get()) - } - } - } - } -} diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreen.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreen.kt deleted file mode 100644 index 1b036f13..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreen.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.twofasapp.externalimport.ui.aegis - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.twofasapp.core.encoding.encodeBase64ToString -import com.twofasapp.resources.R -import com.twofasapp.externalimport.ui.common.ImportDescription -import com.twofasapp.externalimport.ui.common.ImportFilePickerButton -import com.twofasapp.externalimport.ui.common.ImportFileScaffold -import com.twofasapp.navigation.ExternalImportDirections -import com.twofasapp.navigation.ExternalImportRouter -import org.koin.androidx.compose.get - -@Composable -fun AegisScreen( - router: ExternalImportRouter = get(), -) { - - ImportFileScaffold( - title = stringResource(id = R.string.externalimport_aegis), - image = painterResource(id = R.drawable.ic_import_aegis), - description = { ImportDescription(text = "Export your accounts from Aegis to unencrypted JSON file and upload it using \"Choose JSON file\" button. Remember to remove the file after successful import.") }, - actions = { - ImportFilePickerButton( - text = "Choose JSON file", - fileType = "application/json", - onFilePicked = { - router.navigate( - ExternalImportDirections.ImportResult( - type = ExternalImportDirections.ImportResult.Type.Aegis, - content = it.toString().encodeBase64ToString() - ) - ) - } - ) - }, - onBackClick = { router.navigateBack() } - ) -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreenFactory.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreenFactory.kt deleted file mode 100644 index 693e476f..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/aegis/AegisScreenFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.twofasapp.externalimport.ui.aegis - -import androidx.compose.runtime.Composable -import com.twofasapp.externalimport.ui.googleauthenticator.GoogleAuthenticatorScreen - -class AegisScreenFactory { - - @Composable - fun create() { - AegisScreen() - } -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreenFactory.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreenFactory.kt deleted file mode 100644 index 24810959..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.externalimport.ui.googleauthenticator - -import androidx.compose.runtime.Composable - -class GoogleAuthenticatorScreenFactory { - - @Composable - fun create() { - GoogleAuthenticatorScreen() - } -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreen.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreen.kt deleted file mode 100644 index a56c50ea..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreen.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.twofasapp.externalimport.ui.main - -import android.app.Activity -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.twofasapp.design.compose.HeaderEntry -import com.twofasapp.design.compose.SimpleEntry -import com.twofasapp.design.compose.Toolbar -import com.twofasapp.design.theme.textSecondary -import com.twofasapp.resources.R -import com.twofasapp.navigation.ExternalImportDirections -import com.twofasapp.navigation.ExternalImportRouter -import org.koin.androidx.compose.get - -@Composable -fun ExternalImportScreen( - router: ExternalImportRouter = get() -) { - val activity = LocalContext.current as? Activity - - Scaffold( - topBar = { Toolbar(title = stringResource(id = R.string.settings__external_import)) { activity?.onBackPressed() } } - ) { padding -> - LazyColumn(modifier = Modifier.padding(padding)) { - item { HeaderEntry(text = stringResource(id = R.string.externalimport_select_app)) } - - item { - SimpleEntry( - title = stringResource(id = R.string.externalimport_google_authenticator), - image = painterResource(id = R.drawable.ic_import_google_authenticator), - click = { router.navigate(ExternalImportDirections.GoogleAuthenticator) } - ) - - SimpleEntry( - title = stringResource(id = R.string.externalimport_aegis), - image = painterResource(id = R.drawable.ic_aegis), - click = { router.navigate(ExternalImportDirections.Aegis) } - ) - - SimpleEntry( - title = stringResource(id = R.string.externalimport_raivo), - image = painterResource(id = R.drawable.ic_raivo), - click = { router.navigate(ExternalImportDirections.Raivo) } - ) - } - - item { - Text( - text = stringResource(id = R.string.externalimport_description), - style = MaterialTheme.typography.body1, - color = MaterialTheme.colors.textSecondary, - modifier = Modifier - .padding(16.dp) - .padding(start = 56.dp) - ) - } - - } - } -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreenFactory.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreenFactory.kt deleted file mode 100644 index ef6c2a12..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/main/ExternalImportScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.externalimport.ui.main - -import androidx.compose.runtime.Composable - -class ExternalImportScreenFactory{ - - @Composable - fun create() { - ExternalImportScreen() - } -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreen.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreen.kt deleted file mode 100644 index d6495a4d..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreen.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.twofasapp.externalimport.ui.raivo - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.twofasapp.core.encoding.encodeBase64ToString -import com.twofasapp.resources.R -import com.twofasapp.externalimport.ui.common.ImportDescription -import com.twofasapp.externalimport.ui.common.ImportFilePickerButton -import com.twofasapp.externalimport.ui.common.ImportFileScaffold -import com.twofasapp.navigation.ExternalImportDirections -import com.twofasapp.navigation.ExternalImportRouter -import org.koin.androidx.compose.get - -@Composable -fun RaivoScreen(router: ExternalImportRouter = get()) { - - ImportFileScaffold( - title = stringResource(id = R.string.externalimport_raivo), - image = painterResource(id = R.drawable.ic_import_raivo), - description = { ImportDescription(text = "Use the \"Export OTPs to ZIP archive\" option in Raivo's Settings, save a ZIP file, extract it and import the JSON file using the \"Choose JSON file\" button.") }, - actions = { - ImportFilePickerButton( - text = "Choose JSON file", - fileType = "application/json", - onFilePicked = { - router.navigate( - ExternalImportDirections.ImportResult( - type = ExternalImportDirections.ImportResult.Type.Raivo, - content = it.toString().encodeBase64ToString() - ) - ) - } - ) - }, - onBackClick = { router.navigateBack() } - ) -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreenFactory.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreenFactory.kt deleted file mode 100644 index 27271026..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/raivo/RaivoScreenFactory.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.twofasapp.externalimport.ui.raivo - -import androidx.compose.runtime.Composable -import com.twofasapp.externalimport.ui.aegis.AegisScreen -import com.twofasapp.externalimport.ui.googleauthenticator.GoogleAuthenticatorScreen - -class RaivoScreenFactory { - - @Composable - fun create() { - RaivoScreen() - } -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultScreenFactory.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultScreenFactory.kt deleted file mode 100644 index 6b2e2657..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.externalimport.ui.result - -import androidx.compose.runtime.Composable - -class ImportResultScreenFactory { - - @Composable - fun create(type: String, content: String) { - return ImportResultScreen(type, content) - } -} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanScreenFactory.kt b/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanScreenFactory.kt deleted file mode 100644 index 27ff49cf..00000000 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.externalimport.ui.scan - -import androidx.compose.runtime.Composable - -class ImportScanScreenFactory { - - @Composable - fun create(startWithGallery: Boolean) { - ImportScanScreen(startWithGallery) - } -} \ No newline at end of file diff --git a/notifications/.gitignore b/feature/about/.gitignore similarity index 100% rename from notifications/.gitignore rename to feature/about/.gitignore diff --git a/feature/about/build.gradle.kts b/feature/about/build.gradle.kts new file mode 100644 index 00000000..467a35db --- /dev/null +++ b/feature/about/build.gradle.kts @@ -0,0 +1,23 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) + alias(libs.plugins.twofasCompose) +} + +android { + namespace = "com.twofasapp.feature.about" +} + +dependencies { + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(project(":core:locale")) + implementation(project(":core:designsystem")) + implementation(project(":data:session")) + + implementation(libs.bundles.compose) + implementation(libs.bundles.viewModel) + implementation(libs.bundles.accompanist) + implementation(libs.bundles.playReview) + implementation(libs.webkit) +} \ No newline at end of file diff --git a/feature/about/src/main/AndroidManifest.xml b/feature/about/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/about/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/about/src/main/assets/open_source_licenses.html b/feature/about/src/main/assets/open_source_licenses.html similarity index 100% rename from about/src/main/assets/open_source_licenses.html rename to feature/about/src/main/assets/open_source_licenses.html diff --git a/about/src/main/java/com/twofasapp/about/AboutModule.kt b/feature/about/src/main/java/com/twofasapp/feature/about/di/AboutModule.kt similarity index 70% rename from about/src/main/java/com/twofasapp/about/AboutModule.kt rename to feature/about/src/main/java/com/twofasapp/feature/about/di/AboutModule.kt index 217bce9e..2f87457b 100644 --- a/about/src/main/java/com/twofasapp/about/AboutModule.kt +++ b/feature/about/src/main/java/com/twofasapp/feature/about/di/AboutModule.kt @@ -1,12 +1,11 @@ -package com.twofasapp.about +package com.twofasapp.feature.about.di -import com.twofasapp.about.ui.AboutViewModel import com.twofasapp.di.KoinModule +import com.twofasapp.feature.about.ui.about.AboutViewModel import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.dsl.module class AboutModule : KoinModule { - override fun provide() = module { viewModelOf(::AboutViewModel) } diff --git a/feature/about/src/main/java/com/twofasapp/feature/about/navigation/AboutNavigation.kt b/feature/about/src/main/java/com/twofasapp/feature/about/navigation/AboutNavigation.kt new file mode 100644 index 00000000..aa8d171a --- /dev/null +++ b/feature/about/src/main/java/com/twofasapp/feature/about/navigation/AboutNavigation.kt @@ -0,0 +1,40 @@ +package com.twofasapp.feature.about.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.twofasapp.common.navigation.NavGraph +import com.twofasapp.common.navigation.NavNode +import com.twofasapp.feature.about.ui.about.AboutRoute +import com.twofasapp.feature.about.ui.licenses.LicensesRoute + +object AboutGraph : NavGraph { + override val route: String = "about" +} + +private sealed class Node(override val path: String) : NavNode { + override val graph: NavGraph = AboutGraph + + object Main : Node("main") + object Licenses : Node("licenses") +} + +fun NavGraphBuilder.aboutNavigation( + navController: NavHostController, +) { + navigation( + route = AboutGraph.route, + startDestination = Node.Main.route, + ) { + composable(Node.Main.route) { + AboutRoute( + openLicenses = { navController.navigate(Node.Licenses.route) } + ) + } + + composable(Node.Licenses.route) { + LicensesRoute() + } + } +} \ No newline at end of file diff --git a/feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutScreen.kt b/feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutScreen.kt new file mode 100644 index 00000000..a311f30c --- /dev/null +++ b/feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutScreen.kt @@ -0,0 +1,112 @@ +package com.twofasapp.feature.about.ui.about + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.core.app.ShareCompat +import com.google.android.play.core.review.ReviewManagerFactory +import com.twofasapp.designsystem.R +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.designsystem.settings.SettingsDivider +import com.twofasapp.designsystem.settings.SettingsHeader +import com.twofasapp.designsystem.settings.SettingsLink +import com.twofasapp.locale.TwLocale +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun AboutRoute( + openLicenses: () -> Unit, + viewModel: AboutViewModel = koinViewModel() +) { + AboutScreen( + versionName = viewModel.versionName, + onLicensesClick = openLicenses, + onReviewClick = { viewModel.reviewDone() } + ) +} + +@Composable +private fun AboutScreen( + versionName: String, + onLicensesClick: () -> Unit, + onReviewClick: () -> Unit, +) { + val activity = LocalContext.current as Activity + val uriHandler = LocalUriHandler.current + val shareText = TwLocale.strings.aboutTellFriendShareText + + Scaffold( + topBar = { TwTopAppBar(titleText = TwLocale.strings.aboutTitle) } + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + + LazyColumn(modifier = Modifier.weight(1f)) { + + item { SettingsHeader(title = TwLocale.strings.aboutGeneral) } + + item { + SettingsLink(title = TwLocale.strings.aboutWriteReview, icon = TwIcons.Write) { + val manager = ReviewManagerFactory.create(activity) + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val flow = manager.launchReviewFlow( + activity, + task.result + ) + flow.addOnCompleteListener { onReviewClick() } + } + } + } + } + + item { + SettingsLink(title = TwLocale.strings.aboutPrivacyPolicy, icon = TwIcons.LockOpen) { + uriHandler.openUri(TwLocale.links.privacyPolicy) + } + } + + item { + SettingsLink(title = TwLocale.strings.aboutTerms, icon = TwIcons.Terms) { + uriHandler.openUri(TwLocale.links.terms) + } + } + + item { + SettingsLink(title = TwLocale.strings.aboutLicenses, icon = TwIcons.Licenses) { + onLicensesClick() + } + } + + item { SettingsDivider() } + + item { SettingsHeader(title = TwLocale.strings.aboutShare) } + + item { + SettingsLink(title = TwLocale.strings.aboutTellFriend, icon = TwIcons.Share) { + ShareCompat.IntentBuilder(activity) + .setType("text/plain") + .setChooserTitle("Share 2FAS") + .setText(shareText) + .startChooser() + } + } + } + + SettingsLink( + title = "Version $versionName", + image = painterResource(id = R.drawable.logo_2fas), + textColor = TwTheme.color.onSurfaceSecondary + ) + } + } +} diff --git a/feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutViewModel.kt b/feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutViewModel.kt new file mode 100644 index 00000000..445869c9 --- /dev/null +++ b/feature/about/src/main/java/com/twofasapp/feature/about/ui/about/AboutViewModel.kt @@ -0,0 +1,34 @@ +package com.twofasapp.feature.about.ui.about + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.twofasapp.common.coroutines.Dispatchers +import com.twofasapp.common.environment.AppBuild +import com.twofasapp.common.environment.BuildVariant +import com.twofasapp.common.ktx.lowercaseFirstLetter +import com.twofasapp.data.session.SessionRepository +import kotlinx.coroutines.launch + +internal class AboutViewModel( + private val dispatchers: Dispatchers, + private val appBuild: AppBuild, + private val sessionRepository: SessionRepository, +) : ViewModel() { + + val versionName = + if (appBuild.buildVariant != BuildVariant.Release) { + "${appBuild.versionName} (${appBuild.buildVariant.name.lowercaseFirstLetter()})" + } else { + appBuild.versionName + } + + fun reviewDone() { + viewModelScope.launch(dispatchers.io) { + sessionRepository.setRateAppDisplayed(true) + } + // TODO +// rateAppStatusPreference.put( +// rateAppStatusPreference.get().copy(counterStarted = Instant.now(), counterReached = Instant.now()) +// ) + } +} \ No newline at end of file diff --git a/feature/about/src/main/java/com/twofasapp/feature/about/ui/licenses/LicensesScreen.kt b/feature/about/src/main/java/com/twofasapp/feature/about/ui/licenses/LicensesScreen.kt new file mode 100644 index 00000000..7fff0019 --- /dev/null +++ b/feature/about/src/main/java/com/twofasapp/feature/about/ui/licenses/LicensesScreen.kt @@ -0,0 +1,46 @@ +package com.twofasapp.feature.about.ui.licenses + +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.locale.TwLocale + +@Composable +internal fun LicensesRoute() { + LicensesScreen() +} + +@Composable +internal fun LicensesScreen() { + val context = LocalContext.current + + Scaffold( + topBar = { TwTopAppBar(titleText = TwLocale.strings.aboutLicenses) } + ) { padding -> + AndroidView(factory = { + WebView(context).apply { +// if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK) && isNight) { +// WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_ON); +// } + + webViewClient = WebViewClient() + try { + loadUrl("file:///android_asset/open_source_licenses.html") + } catch (e: Exception) { + Toast.makeText( + context, + "There is no WebView installed. Can not display licenses.", + Toast.LENGTH_LONG + ).show() + } + } + }, modifier = Modifier.padding(padding)) + } +} \ No newline at end of file diff --git a/settings/.gitignore b/feature/appsettings/.gitignore similarity index 100% rename from settings/.gitignore rename to feature/appsettings/.gitignore diff --git a/feature/appsettings/build.gradle.kts b/feature/appsettings/build.gradle.kts new file mode 100644 index 00000000..31057a82 --- /dev/null +++ b/feature/appsettings/build.gradle.kts @@ -0,0 +1,21 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) + alias(libs.plugins.twofasCompose) +} + +android { + namespace = "com.twofasapp.feature.appsettings" +} + +dependencies { + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(project(":core:locale")) + implementation(project(":core:designsystem")) + implementation(project(":data:session")) + + implementation(libs.bundles.compose) + implementation(libs.bundles.viewModel) + implementation(libs.bundles.accompanist) +} \ No newline at end of file diff --git a/feature/appsettings/src/main/AndroidManifest.xml b/feature/appsettings/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/appsettings/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/di/AppSettingsModule.kt b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/di/AppSettingsModule.kt new file mode 100644 index 00000000..21ba5e6b --- /dev/null +++ b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/di/AppSettingsModule.kt @@ -0,0 +1,12 @@ +package com.twofasapp.feature.appsettings.di + +import com.twofasapp.di.KoinModule +import com.twofasapp.feature.appsettings.ui.AppSettingsViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +class AppSettingsModule : KoinModule { + override fun provide() = module { + viewModelOf(::AppSettingsViewModel) + } +} \ No newline at end of file diff --git a/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/navigation/AppSettingsNavigation.kt b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/navigation/AppSettingsNavigation.kt new file mode 100644 index 00000000..afb5dda3 --- /dev/null +++ b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/navigation/AppSettingsNavigation.kt @@ -0,0 +1,18 @@ +package com.twofasapp.feature.appsettings.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.twofasapp.common.navigation.NavGraph +import com.twofasapp.feature.appsettings.ui.AppSettingsRoute + +object AppSettingsGraph : NavGraph { + override val route: String = "appsettings" +} + +fun NavGraphBuilder.appSettingsNavigation() { + composable( + AppSettingsGraph.route, + ) { + AppSettingsRoute() + } +} \ No newline at end of file diff --git a/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsScreen.kt b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsScreen.kt new file mode 100644 index 00000000..d79db01b --- /dev/null +++ b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsScreen.kt @@ -0,0 +1,63 @@ +package com.twofasapp.feature.appsettings.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twofasapp.designsystem.common.TwTopAppBar +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun AppSettingsRoute( + viewModel: AppSettingsViewModel = koinViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + AppSettingsScreen( + uiState = uiState, + onShowNextTokenClick = { viewModel.toggleShowNextToken() }, + onAppThemeSelect = { viewModel.setAppTheme() } + ) +} + +@Composable +private fun AppSettingsScreen( + uiState: AppSettingsUiState, + onShowNextTokenClick: () -> Unit, + onAppThemeSelect: () -> Unit, +) { + Scaffold( + topBar = { TwTopAppBar(titleText = "App Settings") } + ) { padding -> + + LazyColumn(Modifier.padding(padding)) { + + } + } +} + +//item { +// SimpleEntry( +// title = stringResource(id = R.string.settings__option_theme), +// subtitle = when (uiState.theme) { +// AppTheme.AUTO -> stringResource(R.string.settings__theme_option_auto) +// AppTheme.LIGHT -> stringResource(R.string.settings__theme_option_light) +// AppTheme.DARK -> stringResource(R.string.settings__theme_option_dark) +// }, +// subtitleGravity = SubtitleGravity.END, +// icon = painterResource(id = R.drawable.ic_option_theme), +// click = { router.navigate(SettingsDirections.Theme) } +// ) +//} +// +//item { +// SwitchEntry( +// title = stringResource(id = R.string.settings__show_next_token), +// icon = painterResource(id = R.drawable.ic_next_token), +// isChecked = uiState.showNextToken, +// switch = { isChecked -> viewModel.changeShowNextToken(isChecked) } +// ) +//} diff --git a/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsUiState.kt b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsUiState.kt new file mode 100644 index 00000000..23aa3af6 --- /dev/null +++ b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsUiState.kt @@ -0,0 +1,4 @@ +package com.twofasapp.feature.appsettings.ui + +class AppSettingsUiState { +} \ No newline at end of file diff --git a/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsViewModel.kt b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsViewModel.kt new file mode 100644 index 00000000..832335c8 --- /dev/null +++ b/feature/appsettings/src/main/java/com/twofasapp/feature/appsettings/ui/AppSettingsViewModel.kt @@ -0,0 +1,55 @@ +package com.twofasapp.feature.appsettings.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.twofasapp.common.coroutines.Dispatchers +import com.twofasapp.data.session.SettingsRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class AppSettingsViewModel( + private val dispatchers: Dispatchers, + private val settingsRepository: SettingsRepository +) : ViewModel() { + + val uiState = MutableStateFlow(AppSettingsUiState()) + + init { + viewModelScope.launch(dispatchers.io) { + settingsRepository.observeAppSettings().collect { settings -> + uiState.update { + it + } + } + } + } + + fun toggleShowNextToken() { + + } + + fun setAppTheme() { + + } + +// init { +// viewModelScope.launch(dispatchers.io()) { +// appThemePreference.flow().collect { +// _uiState.update { state -> state.copy(theme = it) } +// } +// } +// } +// +// fun changeTheme(theme: AppTheme) { +// if (theme == uiState.value.theme) { +// return +// } +// +// appThemePreference.put(theme) +// +// ThemeState.applyTheme(theme) +// +// _uiState.update { it.copy(recreateActivity = true) } +// } +} \ No newline at end of file diff --git a/feature/browserext/.gitignore b/feature/browserext/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/browserext/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/browserext/build.gradle.kts b/feature/browserext/build.gradle.kts new file mode 100644 index 00000000..496403d1 --- /dev/null +++ b/feature/browserext/build.gradle.kts @@ -0,0 +1,23 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) + alias(libs.plugins.twofasCompose) +} + +android { + namespace = "com.twofasapp.feature.browserext" +} + +dependencies { + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(project(":core:locale")) + implementation(project(":core:designsystem")) + + implementation(project(":browserextension")) // TO BE REMOVED + implementation(project(":navigation")) // TO BE REMOVED +0 + implementation(libs.bundles.compose) + implementation(libs.bundles.viewModel) + implementation(libs.bundles.accompanist) +} \ No newline at end of file diff --git a/feature/browserext/src/main/AndroidManifest.xml b/feature/browserext/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/browserext/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/browserext/src/main/java/com/twofasapp/feature/browserext/notification/BrowserExtNavigation.kt b/feature/browserext/src/main/java/com/twofasapp/feature/browserext/notification/BrowserExtNavigation.kt new file mode 100644 index 00000000..0f176a68 --- /dev/null +++ b/feature/browserext/src/main/java/com/twofasapp/feature/browserext/notification/BrowserExtNavigation.kt @@ -0,0 +1,88 @@ +package com.twofasapp.feature.browserext.notification + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import androidx.navigation.navigation +import com.twofasapp.browserextension.ui.browser.BrowserDetailsScreen +import com.twofasapp.browserextension.ui.main.BrowserExtensionScreen +import com.twofasapp.browserextension.ui.pairing.progress.PairingProgressScreen +import com.twofasapp.browserextension.ui.pairing.scan.PairingScanScreen +import com.twofasapp.common.navigation.NavGraph +import com.twofasapp.common.navigation.NavNode +import com.twofasapp.common.navigation.withArg +import com.twofasapp.feature.browserext.notification.NavArg.ExtensionId + +object BrowserExtGraph : NavGraph { + override val route: String = "browserext" +} + +internal object NavArg { + val ExtensionId = navArgument("id") { type = NavType.StringType } +} + +private sealed class Node(override val path: String) : NavNode { + override val graph: NavGraph = BrowserExtGraph + + object Main : Node("main") + object PairingScan : Node("pairing/scan") + object PairingProgress : Node("pairing/progress?extensionId={${ExtensionId.name}}") + object BrowserDetails : Node("details/{${ExtensionId.name}}") + +} + +fun NavGraphBuilder.browserExtNavigation( + navController: NavHostController, +) { + navigation( + route = BrowserExtGraph.route, + startDestination = Node.Main.route + ) { + + composable( + route = Node.Main.route, + ) { + BrowserExtensionScreen( + openPairingScan = { navController.navigate(Node.PairingScan.route) }, + openBrowserDetails = { navController.navigate(Node.BrowserDetails.route.withArg(ExtensionId, it)) }, + ) + } + + composable(Node.PairingScan.route) { + PairingScanScreen( + openPairingProgress = { + navController.navigate(Node.PairingProgress.route.withArg(ExtensionId, it)) { + popUpTo(Node.Main.route) + } + } + ) + } + + composable( + route = Node.PairingProgress.route, + arguments = listOf(ExtensionId) + ) { + PairingProgressScreen( + openMain = { navController.popBackStack(Node.Main.route, false) }, + openPairingScan = { + navController.navigate(Node.PairingScan.route) { + popUpTo(Node.Main.route) + } + }, + extensionId = navController.currentBackStackEntry!!.arguments!!.getString(ExtensionId.name, "") + ) + } + + composable( + route = Node.BrowserDetails.route, + arguments = listOf(ExtensionId) + ) { + BrowserDetailsScreen( + onFinish = { navController.popBackStack(Node.Main.route, false) }, + extensionId = navController.currentBackStackEntry!!.arguments!!.getString(ExtensionId.name, "") + ) + } + } +} \ No newline at end of file diff --git a/feature/externalimport/.gitignore b/feature/externalimport/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/externalimport/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/externalimport/build.gradle.kts b/feature/externalimport/build.gradle.kts similarity index 84% rename from externalimport/build.gradle.kts rename to feature/externalimport/build.gradle.kts index 7de4bff9..ca714b50 100644 --- a/externalimport/build.gradle.kts +++ b/feature/externalimport/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.twofasapp.externalimport" + namespace = "com.twofasapp.feature.externalimport" } protobuf { @@ -28,14 +28,13 @@ protobuf { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":resources")) implementation(project(":extensions")) implementation(project(":core")) implementation(project(":design")) - implementation(project(":environment")) + implementation(project(":prefs")) - implementation(project(":navigation")) implementation(project(":services:domain")) implementation(project(":services")) implementation(project(":qrscanner")) @@ -43,9 +42,15 @@ dependencies { implementation(project(":backup:domain")) implementation(project(":serialization")) + implementation(project(":core:common")) + implementation(project(":core:locale")) + implementation(project(":core:designsystem")) + implementation(project(":data:services")) + implementation(libs.bundles.appCompat) implementation(libs.bundles.compose) implementation(libs.bundles.viewModel) + implementation(libs.bundles.accompanist) implementation(libs.kotlinCoroutines) implementation(libs.webkit) implementation(libs.protobuf) diff --git a/feature/externalimport/src/main/AndroidManifest.xml b/feature/externalimport/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4239aed8 --- /dev/null +++ b/feature/externalimport/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/di/ExternalImportModule.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/di/ExternalImportModule.kt new file mode 100644 index 00000000..0f376c0f --- /dev/null +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/di/ExternalImportModule.kt @@ -0,0 +1,23 @@ +package com.twofasapp.feature.externalimport.di + +import com.twofasapp.di.KoinModule +import com.twofasapp.feature.externalimport.domain.AegisImporter +import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter +import com.twofasapp.feature.externalimport.domain.RaivoImporter +import com.twofasapp.feature.externalimport.ui.result.ImportResultViewModel +import com.twofasapp.feature.externalimport.ui.scan.ImportScanViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.module + +class ExternalImportModule : KoinModule { + + override fun provide() = module { + viewModelOf(::ImportScanViewModel) + viewModelOf(::ImportResultViewModel) + + factoryOf(::GoogleAuthenticatorImporter) + factoryOf(::AegisImporter) + factoryOf(::RaivoImporter) + } +} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/domain/AegisImporter.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/AegisImporter.kt similarity index 98% rename from externalimport/src/main/java/com/twofasapp/externalimport/domain/AegisImporter.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/AegisImporter.kt index f6c3805e..21fd3d21 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/domain/AegisImporter.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/AegisImporter.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.domain +package com.twofasapp.feature.externalimport.domain import android.content.Context import android.net.Uri @@ -10,7 +10,7 @@ import com.twofasapp.services.domain.ConvertOtpLinkToService import kotlinx.serialization.Serializable import java.io.BufferedReader -class AegisImporter( +internal class AegisImporter( private val context: Context, private val jsonSerializer: JsonSerializer, private val convertOtpLinkToService: ConvertOtpLinkToService, diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/domain/ExternalImport.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/ExternalImport.kt similarity index 86% rename from externalimport/src/main/java/com/twofasapp/externalimport/domain/ExternalImport.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/ExternalImport.kt index 9dafc899..47261fbd 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/domain/ExternalImport.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/ExternalImport.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.domain +package com.twofasapp.feature.externalimport.domain import com.twofasapp.prefs.model.ServiceDto diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/domain/ExternalImporter.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/ExternalImporter.kt similarity index 71% rename from externalimport/src/main/java/com/twofasapp/externalimport/domain/ExternalImporter.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/ExternalImporter.kt index 23860fc3..d6bf0037 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/domain/ExternalImporter.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/ExternalImporter.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.domain +package com.twofasapp.feature.externalimport.domain interface ExternalImporter { fun isSchemaSupported(content: String): Boolean diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/domain/GoogleAuthenticatorImporter.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/GoogleAuthenticatorImporter.kt similarity index 98% rename from externalimport/src/main/java/com/twofasapp/externalimport/domain/GoogleAuthenticatorImporter.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/GoogleAuthenticatorImporter.kt index 1e2d7733..2a0225e5 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/domain/GoogleAuthenticatorImporter.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/GoogleAuthenticatorImporter.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.domain +package com.twofasapp.feature.externalimport.domain import android.net.Uri import com.google.common.io.BaseEncoding diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/domain/RaivoImporter.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/RaivoImporter.kt similarity index 97% rename from externalimport/src/main/java/com/twofasapp/externalimport/domain/RaivoImporter.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/RaivoImporter.kt index 59823bad..caf549d3 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/domain/RaivoImporter.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/domain/RaivoImporter.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.domain +package com.twofasapp.feature.externalimport.domain import android.content.Context import android.net.Uri @@ -10,7 +10,7 @@ import com.twofasapp.services.domain.ConvertOtpLinkToService import kotlinx.serialization.Serializable import java.io.BufferedReader -class RaivoImporter( +internal class RaivoImporter( private val context: Context, private val jsonSerializer: JsonSerializer, private val convertOtpLinkToService: ConvertOtpLinkToService, diff --git a/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/navigation/ExternalImportNavigation.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/navigation/ExternalImportNavigation.kt new file mode 100644 index 00000000..288eadf3 --- /dev/null +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/navigation/ExternalImportNavigation.kt @@ -0,0 +1,122 @@ +package com.twofasapp.feature.externalimport.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import androidx.navigation.navigation +import com.twofasapp.common.navigation.NavGraph +import com.twofasapp.common.navigation.NavNode +import com.twofasapp.common.navigation.withArg +import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Aegis +import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.GoogleAuthenticator +import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Raivo +import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Result +import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Scan +import com.twofasapp.feature.externalimport.navigation.ExternalImportNode.Selector +import com.twofasapp.feature.externalimport.ui.aegis.AegisRoute +import com.twofasapp.feature.externalimport.ui.googleauthenticator.GoogleAuthenticatorRoute +import com.twofasapp.feature.externalimport.ui.raivo.RaivoRoute +import com.twofasapp.feature.externalimport.ui.result.ImportResultRoute +import com.twofasapp.feature.externalimport.ui.scan.ImportScanRoute +import com.twofasapp.feature.externalimport.ui.selector.SelectorRoute + +object ExternalImportGraph : NavGraph { + override val route: String = "externalimport" +} + +enum class ImportType { GoogleAuthenticator, Aegis, Raivo } + +private object NavArg { + val ImportType = navArgument("importType") { type = NavType.StringType; } + val ImportContent = navArgument("importContent") { type = NavType.StringType; } + val StartFromGallery = navArgument("startFromGallery") { type = NavType.BoolType; } +} + +private sealed class ExternalImportNode(override val path: String) : NavNode { + override val graph: NavGraph = ExternalImportGraph + + object Selector : ExternalImportNode("selector") + object GoogleAuthenticator : ExternalImportNode("googleauthenticator") + object Aegis : ExternalImportNode("aegis") + object Raivo : ExternalImportNode("raivo") + object Scan : ExternalImportNode("scan?startFromGallery={${NavArg.StartFromGallery.name}}") + object Result : ExternalImportNode("result/{${NavArg.ImportType.name}}/{${NavArg.ImportContent.name}}") +} + +fun NavGraphBuilder.externalImportNavigation( + navController: NavHostController, + onFinish: () -> Unit, +) { + navigation( + route = ExternalImportGraph.route, + startDestination = Selector.route + ) { + + composable(route = Selector.route) { + SelectorRoute( + onGoogleAuthenticatorClick = { navController.navigate(GoogleAuthenticator.route) }, + onAegisClick = { navController.navigate(Aegis.route) }, + onRaivoClick = { navController.navigate(Raivo.route) }, + ) + } + + composable(route = GoogleAuthenticator.route) { + GoogleAuthenticatorRoute( + onScanClick = { + navController.navigate( + Scan.route.withArg(NavArg.StartFromGallery, it) + ) + } + ) + } + + composable(route = Aegis.route) { + AegisRoute(onFilePicked = { content -> + navController.navigate( + Result.route + .withArg(NavArg.ImportType, ImportType.Aegis.name) + .withArg(NavArg.ImportContent, content) + ) + }) + } + + composable(route = Raivo.route) { + RaivoRoute(onFilePicked = { content -> + navController.navigate( + Result.route + .withArg(NavArg.ImportType, ImportType.Raivo.name) + .withArg(NavArg.ImportContent, content) + ) + }) + } + + composable( + route = Scan.route, + arguments = listOf(NavArg.StartFromGallery) + ) { + ImportScanRoute( + startFromGallery = it.arguments?.getBoolean(NavArg.StartFromGallery.name) ?: false, + onScanned = { content -> + navController.navigate( + Result.route + .withArg(NavArg.ImportType, ImportType.GoogleAuthenticator.name) + .withArg(NavArg.ImportContent, content) + ) + } + ) + } + + composable( + route = Result.route, + arguments = listOf(NavArg.ImportType, NavArg.ImportContent) + ) { + ImportResultRoute( + type = it.arguments?.getString(NavArg.ImportType.name).orEmpty(), + content = it.arguments?.getString(NavArg.ImportContent.name).orEmpty(), + onFinish = onFinish + ) + } + } +} diff --git a/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/aegis/AegisScreen.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/aegis/AegisScreen.kt new file mode 100644 index 00000000..2813c2ba --- /dev/null +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/aegis/AegisScreen.kt @@ -0,0 +1,30 @@ +package com.twofasapp.feature.externalimport.ui.aegis + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.twofasapp.core.encoding.encodeBase64ToString +import com.twofasapp.feature.externalimport.ui.common.ImportDescription +import com.twofasapp.feature.externalimport.ui.common.ImportFilePickerButton +import com.twofasapp.feature.externalimport.ui.common.ImportFileScaffold +import com.twofasapp.resources.R + +@Composable +internal fun AegisRoute( + onFilePicked: (String) -> Unit, +) { + + ImportFileScaffold( + title = stringResource(id = R.string.externalimport_aegis), + image = painterResource(id = R.drawable.ic_import_aegis), + description = { ImportDescription(text = "Export your accounts from Aegis to unencrypted JSON file and upload it using \"Choose JSON file\" button. Remember to remove the file after successful import.") } + ) { + ImportFilePickerButton( + text = "Choose JSON file", + fileType = "application/json", + onFilePicked = { + onFilePicked(it.toString().encodeBase64ToString()) + } + ) + } +} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/common/ImportComponents.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/common/ImportComponents.kt similarity index 76% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/common/ImportComponents.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/common/ImportComponents.kt index 527f06b7..66eeeb6e 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/common/ImportComponents.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/common/ImportComponents.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.ui.common +package com.twofasapp.feature.externalimport.ui.common import android.app.Activity import android.content.Intent @@ -7,13 +7,19 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally @@ -23,20 +29,18 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.twofasapp.design.compose.ButtonShape -import com.twofasapp.design.compose.ButtonTextColor -import com.twofasapp.design.compose.Toolbar +import com.twofasapp.designsystem.common.TwButton +import com.twofasapp.designsystem.common.TwTopAppBar @Composable -fun ImportFileScaffold( +internal fun ImportFileScaffold( title: String, image: Painter, description: @Composable () -> Unit, actions: @Composable () -> Unit, - onBackClick: () -> Unit = {}, ) { Scaffold( - topBar = { Toolbar(title = title) { onBackClick.invoke() } } + topBar = { TwTopAppBar(title) } ) { padding -> Column( @@ -73,17 +77,17 @@ fun ImportFileScaffold( } @Composable -fun ImportDescription(text: String) { +internal fun ImportDescription(text: String) { Text( text = text, - style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp, fontSize = 17.sp), + style = MaterialTheme.typography.bodyMedium.copy(lineHeight = 22.sp, fontSize = 15.sp), textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 32.dp) ) } @Composable -fun ImportFilePickerButton( +internal fun ImportFilePickerButton( text: String, fileType: String = "*/*", onFilePicked: (Uri) -> Unit = {}, @@ -96,7 +100,8 @@ fun ImportFilePickerButton( result.data?.data?.let { onFilePicked.invoke(it) } } - Button( + TwButton( + text = text, onClick = { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) .apply { @@ -116,12 +121,8 @@ fun ImportFilePickerButton( } } }, - shape = ButtonShape(), modifier = Modifier .padding(bottom = 24.dp, top = 8.dp) .height(48.dp) - ) { - Text(text = text.uppercase(), color = ButtonTextColor()) - } - + ) } \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreen.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreen.kt similarity index 71% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreen.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreen.kt index 2a3daca0..7645be56 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreen.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/googleauthenticator/GoogleAuthenticatorScreen.kt @@ -1,11 +1,24 @@ -package com.twofasapp.externalimport.ui.googleauthenticator +package com.twofasapp.feature.externalimport.ui.googleauthenticator import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier @@ -14,30 +27,28 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.twofasapp.design.compose.ButtonShape -import com.twofasapp.design.compose.ButtonTextColor -import com.twofasapp.design.compose.Toolbar import com.twofasapp.design.compose.dialogs.RationaleDialog -import com.twofasapp.resources.R -import com.twofasapp.navigation.ExternalImportDirections -import com.twofasapp.navigation.ExternalImportRouter +import com.twofasapp.designsystem.common.TwButton +import com.twofasapp.designsystem.common.TwTextButton +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.permissions.CameraPermissionRequestFlow import com.twofasapp.permissions.PermissionStatus +import com.twofasapp.resources.R import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take import org.koin.androidx.compose.get @Composable -fun GoogleAuthenticatorScreen( - router: ExternalImportRouter = get(), +internal fun GoogleAuthenticatorRoute( + onScanClick: (Boolean) -> Unit, cameraPermissionRequest: CameraPermissionRequestFlow = get(), ) { var showRationaleDialog by remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() Scaffold( - topBar = { Toolbar(title = stringResource(id = R.string.externalimport_google_authenticator)) { router.navigateBack() } } + topBar = { TwTopAppBar(stringResource(id = R.string.externalimport_google_authenticator)) } ) { padding -> Column( @@ -65,52 +76,48 @@ fun GoogleAuthenticatorScreen( Text( text = "Export your accounts from Google Authenticator to a QR code using the \"Transfer Accounts\" option. Then make a screenshot and use the \"Choose QR code\" button below. If you're importing codes from another device, use the \"Scan QR code\" button instead.", - style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp, fontSize = 17.sp), + style = MaterialTheme.typography.bodyMedium.copy(lineHeight = 22.sp, fontSize = 15.sp), textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 32.dp) ) } - Button( + TwButton( + text = "Scan QR code", onClick = { cameraPermissionRequest.execute() .take(1) .onEach { when (it) { - PermissionStatus.GRANTED -> router.navigate(ExternalImportDirections.ImportScan()) + PermissionStatus.GRANTED -> onScanClick(false) PermissionStatus.DENIED -> Unit PermissionStatus.DENIED_NEVER_ASK -> showRationaleDialog = true } }.launchIn(coroutineScope) }, - shape = ButtonShape(), modifier = Modifier .height(48.dp) .align(CenterHorizontally) - ) { - Text(text = "Scan QR code".uppercase(), color = ButtonTextColor()) - } + ) - TextButton( + TwTextButton( + text = "Choose QR code", onClick = { cameraPermissionRequest.execute() .take(1) .onEach { when (it) { - PermissionStatus.GRANTED -> router.navigate(ExternalImportDirections.ImportScan(startWithGallery = true)) + PermissionStatus.GRANTED -> onScanClick(true) PermissionStatus.DENIED -> Unit PermissionStatus.DENIED_NEVER_ASK -> showRationaleDialog = true } }.launchIn(coroutineScope) }, - shape = ButtonShape(), modifier = Modifier .padding(bottom = 16.dp, top = 8.dp) .height(48.dp) .align(CenterHorizontally) - ) { - Text(text = "Choose QR code".uppercase()) - } + ) } if (showRationaleDialog) { diff --git a/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/raivo/RaivoScreen.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/raivo/RaivoScreen.kt new file mode 100644 index 00000000..b098876f --- /dev/null +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/raivo/RaivoScreen.kt @@ -0,0 +1,28 @@ +package com.twofasapp.feature.externalimport.ui.raivo + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.twofasapp.core.encoding.encodeBase64ToString +import com.twofasapp.feature.externalimport.ui.common.ImportDescription +import com.twofasapp.feature.externalimport.ui.common.ImportFilePickerButton +import com.twofasapp.feature.externalimport.ui.common.ImportFileScaffold +import com.twofasapp.resources.R + +@Composable +internal fun RaivoRoute( + onFilePicked: (String) -> Unit, +) { + + ImportFileScaffold( + title = stringResource(id = R.string.externalimport_raivo), + image = painterResource(id = R.drawable.ic_import_raivo), + description = { ImportDescription(text = "Use the \"Export OTPs to ZIP archive\" option in Raivo's Settings, save a ZIP file, extract it and import the JSON file using the \"Choose JSON file\" button.") } + ) { + ImportFilePickerButton( + text = "Choose JSON file", + fileType = "application/json", + onFilePicked = { onFilePicked(it.toString().encodeBase64ToString()) } + ) + } +} \ No newline at end of file diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultScreen.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultScreen.kt similarity index 67% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultScreen.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultScreen.kt index 3a224250..6271b5eb 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultScreen.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultScreen.kt @@ -1,15 +1,20 @@ -package com.twofasapp.externalimport.ui.result +package com.twofasapp.feature.externalimport.ui.result import android.app.Activity import android.widget.Toast import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -20,25 +25,23 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.twofasapp.design.compose.ButtonShape -import com.twofasapp.design.compose.ButtonTextColor -import com.twofasapp.design.compose.Toolbar +import com.twofasapp.designsystem.common.TwButton +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.feature.externalimport.domain.ExternalImport +import com.twofasapp.feature.externalimport.navigation.ImportType import com.twofasapp.resources.R -import com.twofasapp.externalimport.domain.ExternalImport -import com.twofasapp.navigation.ExternalImportDirections.ImportResult.Type -import com.twofasapp.navigation.ExternalImportRouter import org.koin.androidx.compose.get @Composable -internal fun ImportResultScreen( +internal fun ImportResultRoute( type: String, content: String, - viewModel: ImportResultViewModel = get(), - router: ExternalImportRouter = get() + onFinish: () -> Unit, + viewModel: ImportResultViewModel = get() ) { val uiState = viewModel.uiState.collectAsState().value val activity = (LocalContext.current as? Activity) - val importType = Type.valueOf(type) + val importType = ImportType.valueOf(type) LaunchedEffect(Unit) { viewModel.import(importType, content) @@ -46,11 +49,11 @@ internal fun ImportResultScreen( if (uiState.finishSuccess) { Toast.makeText(activity, "Tokens imported successfully!", Toast.LENGTH_LONG).show() - activity?.finish() + onFinish() } Scaffold( - topBar = { Toolbar(title = stringResource(id = R.string.settings__external_import)) { router.navigateBack() } } + topBar = { TwTopAppBar(stringResource(id = R.string.settings__external_import)) } ) { padding -> Column( @@ -71,9 +74,9 @@ internal fun ImportResultScreen( Image( painter = painterResource( id = when (importType) { - Type.GoogleAuthenticator -> R.drawable.ic_import_ga - Type.Aegis -> R.drawable.ic_import_aegis - Type.Raivo -> R.drawable.ic_import_raivo + ImportType.GoogleAuthenticator -> R.drawable.ic_import_ga + ImportType.Aegis -> R.drawable.ic_import_aegis + ImportType.Raivo -> R.drawable.ic_import_raivo } ), contentDescription = null, @@ -84,7 +87,7 @@ internal fun ImportResultScreen( Text( text = uiState.title, - style = MaterialTheme.typography.h6, + style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 16.dp) ) @@ -93,7 +96,7 @@ internal fun ImportResultScreen( Text( text = uiState.description, - style = MaterialTheme.typography.body1, + style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 24.dp) ) @@ -103,7 +106,7 @@ internal fun ImportResultScreen( Text( text = uiState.counter, - style = MaterialTheme.typography.h6, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(horizontal = 24.dp) ) } @@ -113,28 +116,26 @@ internal fun ImportResultScreen( Text( text = uiState.footer, - style = MaterialTheme.typography.body1, + style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(horizontal = 24.dp) ) } } - Button( + TwButton( + text = uiState.button, onClick = { if (uiState.importResult is ExternalImport.Success) { viewModel.saveServices() } else { - router.navigateBack() + activity?.onBackPressed() } }, - shape = ButtonShape(), modifier = Modifier .padding(16.dp) .height(48.dp) .align(Alignment.CenterHorizontally) - ) { - Text(text = uiState.button.uppercase(), color = ButtonTextColor()) - } + ) } } } diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultUiState.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultUiState.kt similarity index 69% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultUiState.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultUiState.kt index 2f52284a..b68cc6dc 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultUiState.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultUiState.kt @@ -1,6 +1,6 @@ -package com.twofasapp.externalimport.ui.result +package com.twofasapp.feature.externalimport.ui.result -import com.twofasapp.externalimport.domain.ExternalImport +import com.twofasapp.feature.externalimport.domain.ExternalImport internal data class ImportResultUiState( val importResult: ExternalImport? = null, diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultViewModel.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultViewModel.kt similarity index 66% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultViewModel.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultViewModel.kt index 87cce07b..e450c23e 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/result/ImportResultViewModel.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/result/ImportResultViewModel.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.ui.result +package com.twofasapp.feature.externalimport.ui.result import androidx.lifecycle.viewModelScope import com.twofasapp.backup.domain.SyncBackupTrigger @@ -6,14 +6,20 @@ import com.twofasapp.backup.domain.SyncBackupWorkDispatcher import com.twofasapp.base.BaseViewModel import com.twofasapp.base.dispatcher.Dispatchers import com.twofasapp.core.encoding.decodeBase64 -import com.twofasapp.externalimport.domain.AegisImporter -import com.twofasapp.externalimport.domain.ExternalImport -import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter -import com.twofasapp.externalimport.domain.RaivoImporter -import com.twofasapp.navigation.ExternalImportDirections.ImportResult.Type +import com.twofasapp.feature.externalimport.domain.AegisImporter +import com.twofasapp.feature.externalimport.domain.ExternalImport +import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter +import com.twofasapp.feature.externalimport.domain.RaivoImporter +import com.twofasapp.feature.externalimport.navigation.ImportType import com.twofasapp.services.data.converter.toService import com.twofasapp.services.domain.AddServiceCase -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch internal class ImportResultViewModel( @@ -28,12 +34,12 @@ internal class ImportResultViewModel( private val _uiState = MutableStateFlow(ImportResultUiState()) val uiState = _uiState.asStateFlow() - fun import(type: Type, content: String) { + fun import(type: ImportType, content: String) { viewModelScope.launch(dispatchers.io()) { val result = when (type) { - Type.GoogleAuthenticator -> googleAuthenticatorImporter.read(content.decodeBase64()) - Type.Aegis -> aegisImporter.read(content.decodeBase64()) - Type.Raivo -> raivoImporter.read(content.decodeBase64()) + ImportType.GoogleAuthenticator -> googleAuthenticatorImporter.read(content.decodeBase64()) + ImportType.Aegis -> aegisImporter.read(content.decodeBase64()) + ImportType.Raivo -> raivoImporter.read(content.decodeBase64()) } when (result) { @@ -53,6 +59,7 @@ internal class ImportResultViewModel( ) } } + is ExternalImport.ParsingError, ExternalImport.UnsupportedError -> { _uiState.update { @@ -85,15 +92,15 @@ internal class ImportResultViewModel( } } - private fun getTitle(type: Type) = when (type) { - Type.GoogleAuthenticator -> "Importing 2FA tokens from Google Authenticator app" - Type.Aegis -> "Importing 2FA tokens from Aegis app" - Type.Raivo -> "Importing 2FA tokens from Raivo app" + private fun getTitle(type: ImportType) = when (type) { + ImportType.GoogleAuthenticator -> "Importing 2FA tokens from Google Authenticator app" + ImportType.Aegis -> "Importing 2FA tokens from Aegis app" + ImportType.Raivo -> "Importing 2FA tokens from Raivo app" } - private fun getSuccessDescription(type: Type) = when (type) { - Type.GoogleAuthenticator -> "This QR code allows importing tokens from Google Authenticator." - Type.Aegis -> "This JSON file allows importing tokens from Aegis." - Type.Raivo -> "This JSON file allows importing tokens from Raivo." + private fun getSuccessDescription(type: ImportType) = when (type) { + ImportType.GoogleAuthenticator -> "This QR code allows importing tokens from Google Authenticator." + ImportType.Aegis -> "This JSON file allows importing tokens from Aegis." + ImportType.Raivo -> "This JSON file allows importing tokens from Raivo." } } diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanScreen.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanScreen.kt similarity index 55% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanScreen.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanScreen.kt index be50b9b4..0c559e1e 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanScreen.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanScreen.kt @@ -1,46 +1,35 @@ -package com.twofasapp.externalimport.ui.scan +package com.twofasapp.feature.externalimport.ui.scan import android.app.Activity -import androidx.compose.material.Scaffold +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.twofasapp.core.encoding.encodeBase64ToString -import com.twofasapp.design.compose.Toolbar import com.twofasapp.design.dialogs.InfoDialog -import com.twofasapp.navigation.ExternalImportDirections -import com.twofasapp.navigation.ExternalImportRouter +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.qrscanner.ui.QrScannerScreen import org.koin.androidx.compose.get @Composable -internal fun ImportScanScreen( - startWithGallery: Boolean, +internal fun ImportScanRoute( + startFromGallery: Boolean, viewModel: ImportScanViewModel = get(), - router: ExternalImportRouter = get(), + onScanned: (String) -> Unit, ) { val uiState = viewModel.uiState.collectAsState() val activity = (LocalContext.current as? Activity) Scaffold( - topBar = { - Toolbar( - title = "Scan QR Code", - ) { - router.navigateBack() - } - } + topBar = { TwTopAppBar("Scan QR Code") } ) { padding -> - QrScannerScreen(isGalleryEnabled = true, startWithGallery = startWithGallery) + QrScannerScreen(isGalleryEnabled = true, startWithGallery = startFromGallery, modifier = Modifier.padding(padding)) if (uiState.value.isSuccess) { - router.navigate( - ExternalImportDirections.ImportResult( - type = ExternalImportDirections.ImportResult.Type.GoogleAuthenticator, - content = uiState.value.content.encodeBase64ToString() - ) - ) + onScanned(uiState.value.content.encodeBase64ToString()) } if (uiState.value.showErrorDialog) { diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanUiState.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanUiState.kt similarity index 83% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanUiState.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanUiState.kt index d350c77b..59261023 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanUiState.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanUiState.kt @@ -1,4 +1,4 @@ -package com.twofasapp.externalimport.ui.scan +package com.twofasapp.feature.externalimport.ui.scan internal data class ImportScanUiState( val isSuccess: Boolean = false, diff --git a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanViewModel.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanViewModel.kt similarity index 94% rename from externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanViewModel.kt rename to feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanViewModel.kt index 376d2211..a3670389 100644 --- a/externalimport/src/main/java/com/twofasapp/externalimport/ui/scan/ImportScanViewModel.kt +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/scan/ImportScanViewModel.kt @@ -1,11 +1,11 @@ -package com.twofasapp.externalimport.ui.scan +package com.twofasapp.feature.externalimport.ui.scan import android.net.Uri import androidx.lifecycle.viewModelScope import com.twofasapp.base.BaseViewModel import com.twofasapp.base.dispatcher.Dispatchers import com.twofasapp.resources.R -import com.twofasapp.externalimport.domain.GoogleAuthenticatorImporter +import com.twofasapp.feature.externalimport.domain.GoogleAuthenticatorImporter import com.twofasapp.qrscanner.domain.ScanQr import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/selector/SelectorScreen.kt b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/selector/SelectorScreen.kt new file mode 100644 index 00000000..c7d0afcd --- /dev/null +++ b/feature/externalimport/src/main/java/com/twofasapp/feature/externalimport/ui/selector/SelectorScreen.kt @@ -0,0 +1,74 @@ +package com.twofasapp.feature.externalimport.ui.selector + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.twofasapp.designsystem.R +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.designsystem.settings.SettingsDescription +import com.twofasapp.designsystem.settings.SettingsHeader +import com.twofasapp.designsystem.settings.SettingsLink +import com.twofasapp.locale.TwLocale + +@Composable +internal fun SelectorRoute( + onGoogleAuthenticatorClick: () -> Unit, + onAegisClick: () -> Unit, + onRaivoClick: () -> Unit, +) { + SelectorScreen( + onGoogleAuthenticatorClick = onGoogleAuthenticatorClick, + onAegisClick = onAegisClick, + onRaivoClick = onRaivoClick + ) +} + +@Composable +private fun SelectorScreen( + onGoogleAuthenticatorClick: () -> Unit, + onAegisClick: () -> Unit, + onRaivoClick: () -> Unit, +) { + + Scaffold( + topBar = { TwTopAppBar(TwLocale.strings.externalImportTitle) } + ) { padding -> + + LazyColumn(modifier = Modifier.padding(padding)) { + item { + SettingsHeader(title = TwLocale.strings.externalImportHeader) + } + + item { + SettingsLink( + title = TwLocale.strings.externalImportGoogleAuthenticator, + image = painterResource(id = R.drawable.logo_google_authenticator), + onClick = onGoogleAuthenticatorClick + ) + } + + item { + SettingsLink( + title = TwLocale.strings.externalImportAegis, + image = painterResource(id = R.drawable.logo_aegis), + onClick = onAegisClick + ) + } + + item { + SettingsLink( + title = TwLocale.strings.externalImportRaivo, + image = painterResource(id = R.drawable.logo_raivo), + onClick = onRaivoClick + ) + } + + item { + SettingsDescription(text = TwLocale.strings.externalImportNotice) + } + } + } +} \ No newline at end of file diff --git a/externalimport/src/main/proto/google_authenticator.proto b/feature/externalimport/src/main/proto/google_authenticator.proto similarity index 100% rename from externalimport/src/main/proto/google_authenticator.proto rename to feature/externalimport/src/main/proto/google_authenticator.proto diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 39984d75..e520264f 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -9,8 +9,9 @@ android { } dependencies { - implementation(project(":di")) - implementation(project(":data:session")) + implementation(project(":core:di")) + implementation(project(":data:notifications")) + implementation(project(":data:services")) implementation(project(":core:common")) implementation(project(":core:locale")) implementation(project(":core:designsystem")) diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/di/HomeModule.kt b/feature/home/src/main/java/com/twofasapp/feature/home/di/HomeModule.kt index a67aaa18..b21bf997 100644 --- a/feature/home/src/main/java/com/twofasapp/feature/home/di/HomeModule.kt +++ b/feature/home/src/main/java/com/twofasapp/feature/home/di/HomeModule.kt @@ -1,10 +1,19 @@ package com.twofasapp.feature.home.di import com.twofasapp.di.KoinModule +import com.twofasapp.feature.home.ui.bottombar.BottomBarViewModel +import com.twofasapp.feature.home.ui.notifications.NotificationsViewModel +import com.twofasapp.feature.home.ui.services.ServicesViewModel +import com.twofasapp.feature.home.ui.settings.SettingsViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.dsl.module class HomeModule : KoinModule { override fun provide() = module { + viewModelOf(::BottomBarViewModel) + viewModelOf(::ServicesViewModel) + viewModelOf(::SettingsViewModel) + viewModelOf(::NotificationsViewModel) } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/twofasapp/feature/home/navigation/HomeNavigation.kt index 2a35f029..f8a14eff 100644 --- a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/twofasapp/feature/home/navigation/HomeNavigation.kt @@ -1,18 +1,94 @@ package com.twofasapp.feature.home.navigation +import android.app.Activity +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import androidx.navigation.navigation import com.twofasapp.common.navigation.NavGraph -import com.twofasapp.feature.home.ui.HomeRoute +import com.twofasapp.common.navigation.NavNode +import com.twofasapp.feature.home.ui.bottombar.BottomBarListener +import com.twofasapp.feature.home.ui.notifications.NotificationsRoute +import com.twofasapp.feature.home.ui.services.ServicesRoute +import com.twofasapp.feature.home.ui.settings.SettingsRoute object HomeGraph : NavGraph { override val route: String = "home" } +internal sealed class HomeNode(override val path: String) : NavNode { + override val graph: NavGraph = HomeGraph + + object Services : HomeNode("services") + object Settings : HomeNode("settings") + object Notifications : HomeNode("notifications") +} + fun NavGraphBuilder.homeNavigation( + navController: NavController, + listener: HomeNavigationListener, ) { - composable(HomeGraph.route) { - HomeRoute( - ) + val bottomBarListener = object : BottomBarListener { + override fun openHome() { + navController.navigate(HomeNode.Services.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + + override fun openSettings() { + navController.navigate(HomeNode.Settings.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + + } + + override fun openNotifications() { + navController.navigate(HomeNode.Notifications.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } } + + navigation( + route = HomeGraph.route, + startDestination = HomeNode.Services.route + ) { + composable(HomeNode.Services.route) { + ServicesRoute(listener, bottomBarListener) + } + + composable(HomeNode.Settings.route) { + SettingsRoute(listener, bottomBarListener) + } + + composable(HomeNode.Notifications.route) { + NotificationsRoute(bottomBarListener) + } + } +} + +interface HomeNavigationListener { + fun openAddManuallyService(activity: Activity) + fun openAddQrService(activity: Activity) + fun openService(activity: Activity, serviceId: Long) + fun openExternalImport() + fun openBrowserExt() + fun openSecurity(activity: Activity) + fun openBackup(activity: Activity) + fun openAppSettings() + fun openTrash() + fun openAbout() } \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/NotificationsNavigation.kt b/feature/home/src/main/java/com/twofasapp/feature/home/navigation/NotificationsNavigation.kt deleted file mode 100644 index 189e0270..00000000 --- a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/NotificationsNavigation.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.twofasapp.feature.home.navigation - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.twofasapp.common.navigation.NavGraph - -object NotificationsGraph : NavGraph { - override val route: String = "notifications" -} - -fun NavGraphBuilder.notificationsNavigation( -) { - composable(NotificationsGraph.route) { - Box(modifier = Modifier.fillMaxSize()) { - Text(text = "Notifications", modifier = Modifier.align(Alignment.Center)) - } - } -} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/ServicesNavigation.kt b/feature/home/src/main/java/com/twofasapp/feature/home/navigation/ServicesNavigation.kt deleted file mode 100644 index f18ac2ae..00000000 --- a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/ServicesNavigation.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.twofasapp.feature.home.navigation - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.twofasapp.common.navigation.NavGraph - -object ServicesGraph : NavGraph { - override val route: String = "services" -} - -fun NavGraphBuilder.servicesNavigation( -) { - composable(ServicesGraph.route) { - Box(modifier = Modifier.fillMaxSize()) { - Text(text = "Services", modifier = Modifier.align(Alignment.Center)) - } - } -} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/SettingsNavigation.kt b/feature/home/src/main/java/com/twofasapp/feature/home/navigation/SettingsNavigation.kt deleted file mode 100644 index c6ad532e..00000000 --- a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/SettingsNavigation.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.twofasapp.feature.home.navigation - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.twofasapp.common.navigation.NavGraph - -object SettingsGraph : NavGraph { - override val route: String = "settings" -} - -fun NavGraphBuilder.settingsNavigation( -) { - composable(SettingsGraph.route) { - Box(modifier = Modifier.fillMaxSize()) { - Text(text = "Settings", modifier = Modifier.align(Alignment.Center)) - } - } -} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/HomeScreen.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/HomeScreen.kt deleted file mode 100644 index aa55890f..00000000 --- a/feature/home/src/main/java/com/twofasapp/feature/home/ui/HomeScreen.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.twofasapp.feature.home.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import com.twofasapp.designsystem.composable.TwsNavigationBar -import com.twofasapp.designsystem.composable.TwsNavigationBarItem -import com.twofasapp.feature.home.R -import com.twofasapp.feature.home.navigation.BottomNavItem -import com.twofasapp.feature.home.navigation.NotificationsGraph -import com.twofasapp.feature.home.navigation.ServicesGraph -import com.twofasapp.feature.home.navigation.SettingsGraph -import com.twofasapp.feature.home.navigation.notificationsNavigation -import com.twofasapp.feature.home.navigation.servicesNavigation -import com.twofasapp.feature.home.navigation.settingsNavigation - -@Composable -internal fun HomeRoute() { - HomeScreen() -} - -@Composable -private fun HomeScreen() { - val bottomNavItems = listOf( - BottomNavItem( - title = "Tokens", - icon = painterResource(id = R.drawable.ic_android), - route = ServicesGraph.route, - ), - BottomNavItem( - title = "Settings", - icon = painterResource(id = R.drawable.ic_android), - route = SettingsGraph.route, - ), - BottomNavItem( - title = "Notifications", - icon = painterResource(id = R.drawable.ic_android), - route = NotificationsGraph.route, - ), - ) - - val homeNavController = rememberNavController() - val navBackStackState = homeNavController.currentBackStackEntryAsState().value - - Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Bottom) { - - NavHost( - navController = homeNavController, - startDestination = ServicesGraph.route, - modifier = Modifier - .fillMaxSize() - .weight(1f), - ) { - servicesNavigation() - settingsNavigation() - notificationsNavigation() - } - - TwsNavigationBar { - bottomNavItems.forEach { - TwsNavigationBarItem( - text = it.title, - icon = it.icon, - selected = navBackStackState?.destination?.route == it.route, - onClick = { - homeNavController.navigate(it.route) { - popUpTo(homeNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - ) - } - } - } -} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/bottombar/BottomBar.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/bottombar/BottomBar.kt new file mode 100644 index 00000000..491471e1 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/bottombar/BottomBar.kt @@ -0,0 +1,76 @@ +package com.twofasapp.feature.home.ui.bottombar + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import com.twofasapp.data.notifications.NotificationsRepository +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.common.TwNavigationBar +import com.twofasapp.designsystem.common.TwNavigationBarItem +import com.twofasapp.feature.home.navigation.HomeNode +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import org.koin.androidx.compose.koinViewModel + +private val bottomNavItems + @Composable + get() = listOf( + BottomNavItem( + title = "Tokens", + icon = TwIcons.Home, + route = HomeNode.Services.route, + ), + BottomNavItem( + title = "Settings", + icon = TwIcons.Settings, + route = HomeNode.Settings.route, + ), + BottomNavItem( + title = "Notifications", + icon = TwIcons.Notification, + route = HomeNode.Notifications.route, + ), + ) + +interface BottomBarListener { + fun openHome() + fun openSettings() + fun openNotifications() +} + +@Composable +internal fun BottomBar( + selectedIndex: Int, + listener: BottomBarListener, + viewModel: BottomBarViewModel = koinViewModel(), +) { + val hasUnreadNotifications by viewModel.hasUnreadNotifications.collectAsStateWithLifecycle() + + TwNavigationBar { + bottomNavItems.forEachIndexed { index, item -> + TwNavigationBarItem( + text = item.title, + icon = item.icon, + selected = index == selectedIndex, + showBadge = index == 2 && hasUnreadNotifications, + onClick = { + when { + index == 0 && selectedIndex != 0 -> listener.openHome() + index == 1 && selectedIndex != 1 -> listener.openSettings() + index == 2 && selectedIndex != 2 -> listener.openNotifications() + } + } + ) + } + } +} + +internal class BottomBarViewModel( + notificationsRepository: NotificationsRepository, +) : ViewModel() { + + val hasUnreadNotifications = notificationsRepository.hasUnreadNotifications() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/BottomNavItem.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/bottombar/BottomNavItem.kt similarity index 76% rename from feature/home/src/main/java/com/twofasapp/feature/home/navigation/BottomNavItem.kt rename to feature/home/src/main/java/com/twofasapp/feature/home/ui/bottombar/BottomNavItem.kt index 33281276..20933656 100644 --- a/feature/home/src/main/java/com/twofasapp/feature/home/navigation/BottomNavItem.kt +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/bottombar/BottomNavItem.kt @@ -1,4 +1,4 @@ -package com.twofasapp.feature.home.navigation +package com.twofasapp.feature.home.ui.bottombar import androidx.compose.ui.graphics.painter.Painter diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsScreen.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsScreen.kt new file mode 100644 index 00000000..20538511 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsScreen.kt @@ -0,0 +1,149 @@ +package com.twofasapp.feature.home.ui.notifications + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twofasapp.data.notifications.domain.Notification +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.feature.home.R +import com.twofasapp.feature.home.ui.bottombar.BottomBar +import com.twofasapp.feature.home.ui.bottombar.BottomBarListener +import com.twofasapp.locale.TwLocale +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun NotificationsRoute( + bottomBarListener: BottomBarListener, + viewModel: NotificationsViewModel = koinViewModel(), +) { + val notifications by viewModel.notificationsList.collectAsStateWithLifecycle() + + NotificationsScreen( + bottomBarListener = bottomBarListener, + notifications = notifications, + onNotificationClick = { viewModel.onNotificationClick(it) } + ) +} + +@Composable +private fun NotificationsScreen( + bottomBarListener: BottomBarListener, + notifications: List, + onNotificationClick: (Notification) -> Unit, +) { + val uriHandler = LocalUriHandler.current + + Scaffold( + bottomBar = { BottomBar(2, bottomBarListener) }, + topBar = { TwTopAppBar(titleText = TwLocale.strings.notificationsTitle, showBackButton = false) }, + ) { padding -> + + LazyColumn(Modifier.padding(padding)) { + + if (notifications.isEmpty()) { + item { + Box( + Modifier + .fillMaxWidth() + .padding(24.dp) + ) { + Text( + text = TwLocale.strings.notificationsEmpty, + style = TwTheme.typo.body3, + color = TwTheme.color.onSurfacePrimary, + modifier = Modifier.align(Alignment.Center) + ) + } + } + + return@LazyColumn + } + + items(notifications, key = { it.id }) { notification -> + Notification( + notification = notification, + modifier = Modifier + .clickable { + onNotificationClick(notification) + uriHandler.openUri(notification.link) + } + .background(if (notification.isRead) TwTheme.color.surface else TwTheme.color.background) + .padding(16.dp) + ) + Divider(color = TwTheme.color.divider) + } + } + } +} + +@Composable +private fun Notification( + notification: Notification, + modifier: Modifier = Modifier, +) { + Row(modifier) { + Image( + painter = painterResource( + when (notification.category) { + Notification.Category.Updates -> R.drawable.notif_category_update + Notification.Category.News -> R.drawable.notif_category_news + Notification.Category.Features -> R.drawable.notif_category_feature + Notification.Category.Youtube -> R.drawable.notif_category_video + } + ), + contentDescription = null, + modifier = Modifier + .size(40.dp) + .padding(top = 6.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(Modifier.weight(1f)) { + Text( + text = notification.message, + modifier = Modifier.fillMaxWidth(), + color = TwTheme.color.onSurfacePrimary, + style = TwTheme.typo.body3 + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = TwLocale.formatDuration(notification.publishTime), + modifier = Modifier.fillMaxWidth(), + color = TwTheme.color.onSurfaceSecondary, + style = TwTheme.typo.body4 + ) + } + + Spacer(modifier = Modifier.width(24.dp)) + + Icon(painter = TwIcons.ExternalLink, contentDescription = null, tint = TwTheme.color.iconTint) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsViewModel.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsViewModel.kt new file mode 100644 index 00000000..0fafba7e --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/notifications/NotificationsViewModel.kt @@ -0,0 +1,43 @@ +package com.twofasapp.feature.home.ui.notifications + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.twofasapp.common.analytics.Analytics +import com.twofasapp.common.analytics.AnalyticsEvent +import com.twofasapp.common.analytics.AnalyticsParam +import com.twofasapp.data.notifications.NotificationsRepository +import com.twofasapp.data.notifications.domain.Notification +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class NotificationsViewModel( + private val analytics: Analytics, + private val notificationsRepository: NotificationsRepository, +) : ViewModel() { + + val notificationsList = MutableStateFlow>(emptyList()) + + init { + viewModelScope.launch { + val notifications = notificationsRepository.getNotifications() + notificationsRepository.readAllNotifications() + + notificationsList.update { notifications } + } + } + + fun onNotificationClick(notification: Notification) { + analytics.captureEvent(AnalyticsEvent.NEWS_CLICK, AnalyticsParam.ID to notification.id) + + notificationsList.update { + it.map { item -> + if (item.id == notification.id) { + item.copy(isRead = true) + } else { + item + } + } + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/AppBar.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/AppBar.kt new file mode 100644 index 00000000..8fb76e3d --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/AppBar.kt @@ -0,0 +1,142 @@ +package com.twofasapp.feature.home.ui.services + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.R +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwIconButton +import com.twofasapp.designsystem.common.TwTopAppBar + +@Composable +internal fun ServicesAppBar( + isInEditMode: Boolean, + onEditModeChange: () -> Unit, + scrollBehavior: TopAppBarScrollBehavior, +) { + AnimatedVisibility( + visible = isInEditMode, + enter = fadeIn(), + exit = fadeOut(), + ) { + TwTopAppBar( + titleText = "Manage list", + onBackClick = onEditModeChange, + scrollBehavior = scrollBehavior + ) + } + + AnimatedVisibility( + visible = isInEditMode.not(), + enter = fadeIn(), + exit = fadeOut(), + ) { + TwTopAppBar( + title = { + SearchBar( + modifier = Modifier + .padding(end = 16.dp) + .height(56.dp), + onToggleEditMode = onEditModeChange, + ) + }, + showBackButton = false, + scrollBehavior = scrollBehavior, + ) + } +} + +@Composable +private fun SearchBar( + modifier: Modifier, + onToggleEditMode: () -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + + val animVisibleState = remember { MutableTransitionState(false) } + animVisibleState.targetState = true + val transition = updateTransition(animVisibleState) + + AnimatedVisibility(visible = true) { + + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(28.dp)) + .background(TwTheme.color.surface) + .padding(start = 16.dp, end = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.logo_2fas), contentDescription = null, modifier = Modifier.size(24.dp) + ) + + + TextField( + value = "", + onValueChange = {}, + modifier = Modifier.weight(1f), + placeholder = { Text(text = "Search") }, + colors = TextFieldDefaults.textFieldColors( + textColor = Color.Gray, + disabledTextColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ) + ) + + + Box { + TwIconButton(painter = TwIcons.More, onClick = { expanded = true }) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .widthIn(min = 160.dp) + .background(TwTheme.color.surface) + ) { + DropdownMenuItem( + text = { Text("Manage list", modifier = Modifier.padding(start = 16.dp)) }, + onClick = { + onToggleEditMode() + expanded = false + }, + modifier = Modifier.background(TwTheme.color.surface) + ) + + } + } + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Empty.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Empty.kt new file mode 100644 index 00000000..1b3b3c11 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Empty.kt @@ -0,0 +1,34 @@ +package com.twofasapp.feature.home.ui.services + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.twofasapp.designsystem.common.TwOutlinedButton +import com.twofasapp.feature.home.R +import com.twofasapp.locale.TwLocale + +@Composable +internal fun ServicesEmpty( + modifier: Modifier = Modifier, + onExternalImportClick: () -> Unit = {}, +) { + Column( + modifier = modifier.padding(bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image(painter = painterResource(id = R.drawable.img_services_empty), null, modifier = Modifier.height(120.dp)) + Text(text = TwLocale.strings.servicesEmptyBody, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) + TwOutlinedButton(text = TwLocale.strings.servicesEmptyImportCta, onClick = onExternalImportClick) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Fab.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Fab.kt new file mode 100644 index 00000000..1c5e46d8 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Fab.kt @@ -0,0 +1,51 @@ +package com.twofasapp.feature.home.ui.services + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.locale.TwLocale + +@OptIn(ExperimentalAnimationApi::class) +@Composable +internal fun ServicesFab( + isVisible: Boolean, + isExtendedVisible: Boolean, + isNormalVisible: Boolean, + onClick: () -> Unit, +) { + + if (isVisible) { + if (isExtendedVisible) { + ExtendedFloatingActionButton( + onClick = onClick, + icon = { Icon(Icons.Filled.Add, null) }, + text = { Text(text = TwLocale.strings.servicesEmptyPairServiceCta) }, + containerColor = TwTheme.color.button, + contentColor = TwTheme.color.onButton, + ) + } else { + AnimatedVisibility( + visible = isNormalVisible, + enter = scaleIn(tween(150)), + exit = scaleOut(tween(150)), + ) { + FloatingActionButton( + onClick = onClick, + content = { Icon(Icons.Filled.Add, null) }, + containerColor = TwTheme.color.button, + contentColor = TwTheme.color.onButton, + ) + } + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ListItem.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ListItem.kt new file mode 100644 index 00000000..73d5db32 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ListItem.kt @@ -0,0 +1,13 @@ +package com.twofasapp.feature.home.ui.services + +import com.twofasapp.designsystem.lazy.ListItem + +sealed class ServicesListItem( + override val key: Any, + override val type: Any, +) : ListItem { + object Loader : ServicesListItem("loader", "loader") + object Empty : ServicesListItem("empty", "empty") + data class Service(val id: Long) : ServicesListItem("service_$id", "service") + data class Group(val id: Long) : ServicesListItem("group_$id", "group") +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ModalType.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ModalType.kt new file mode 100644 index 00000000..cf6960c4 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ModalType.kt @@ -0,0 +1,6 @@ +package com.twofasapp.feature.home.ui.services + +internal sealed interface ModalType { + object AddService : ModalType + data class FocusService(val id: Long) : ModalType +} diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Progress.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Progress.kt new file mode 100644 index 00000000..8af50f68 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/Progress.kt @@ -0,0 +1,16 @@ +package com.twofasapp.feature.home.ui.services + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.twofasapp.designsystem.common.TwCircularProgressIndicator + +@Composable +internal fun ServicesProgress( + modifier: Modifier = Modifier, +) { + Box(modifier, Alignment.Center) { + TwCircularProgressIndicator() + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesScreen.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesScreen.kt new file mode 100644 index 00000000..741e7192 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesScreen.kt @@ -0,0 +1,245 @@ +package com.twofasapp.feature.home.ui.services + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.ModalBottomSheet +import com.twofasapp.designsystem.common.isScrollingUp +import com.twofasapp.designsystem.ktx.copyToClipboard +import com.twofasapp.designsystem.ktx.currentActivity +import com.twofasapp.designsystem.lazy.listItem +import com.twofasapp.designsystem.lazy.listItems +import com.twofasapp.designsystem.service.Service +import com.twofasapp.designsystem.service.ServiceStyle +import com.twofasapp.feature.home.navigation.HomeNavigationListener +import com.twofasapp.feature.home.ui.bottombar.BottomBar +import com.twofasapp.feature.home.ui.bottombar.BottomBarListener +import com.twofasapp.feature.home.ui.services.modal.AddServiceModal +import com.twofasapp.feature.home.ui.services.modal.FocusServiceModal +import kotlinx.coroutines.launch +import org.burnoutcrew.reorderable.ReorderableItem +import org.burnoutcrew.reorderable.detectReorderAfterLongPress +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun ServicesRoute( + listener: HomeNavigationListener, + bottomBarListener: BottomBarListener, + onExternalImportClick: () -> Unit = {}, + viewModel: ServicesViewModel = koinViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + ServicesScreen( + uiState = uiState, + listener = listener, + bottomBarListener = bottomBarListener, + onEventConsumed = { viewModel.consumeEvent(it) }, + onFabClick = { viewModel.toggleAddMenu() }, + onExternalImportClick = onExternalImportClick, + onEditModeChange = { viewModel.toggleEditMode() }, + ) +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) +@Composable +private fun ServicesScreen( + uiState: ServicesUiState, + listener: HomeNavigationListener, + bottomBarListener: BottomBarListener, + onEventConsumed: (ServicesStateEvent) -> Unit, + onFabClick: () -> Unit = {}, + onExternalImportClick: () -> Unit = {}, + onEditModeChange: () -> Unit = {}, +) { + + val topAppBarState = rememberTopAppBarState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState) + val modalState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + var modalType by remember { mutableStateOf(ModalType.AddService) } + val activity = LocalContext.currentActivity + val scope = rememberCoroutineScope() + val reorderableState = rememberReorderableLazyListState( + maxScrollPerFrame = 40.dp, + onMove = { from, to -> +// viewModel.orderList.update { +// it.toMutableList().apply { +// add(to.index - 1, removeAt(from.index - 1)) +// } +// } + } + ) + + uiState.events.firstOrNull()?.let { + when (it) { + ServicesStateEvent.ShowAddServiceModal -> { + modalType = ModalType.AddService + scope.launch { modalState.show() } + } + } + onEventConsumed(it) + } + + BackHandler( + enabled = uiState.isInEditMode || modalState.isVisible + ) { + when { + modalState.isVisible -> scope.launch { modalState.hide() } + uiState.isInEditMode -> onEditModeChange() + } + } + + ModalBottomSheet( + sheetState = modalState, + sheetContent = { + when (modalType) { + is ModalType.AddService -> + AddServiceModal( + onAddManuallyClick = { + listener.openAddManuallyService(activity) + scope.launch { modalState.hide() } + }, + onScanQrClick = { + listener.openAddQrService(activity) + scope.launch { modalState.hide() } + } + ) + + is ModalType.FocusService -> { + val id = (modalType as ModalType.FocusService).id + + FocusServiceModal( + serviceState = uiState.getService(id).asState(), + onEditClick = { + listener.openService(activity, (modalType as ModalType.FocusService).id) + scope.launch { modalState.hide() } + }, + onCopyClick = { + activity.copyToClipboard( + uiState.getService(id).code?.current.toString() + ) + scope.launch { modalState.hide() } + } + ) + } + } + } + ) { + Scaffold( + bottomBar = { BottomBar(0, bottomBarListener) }, + topBar = { + ServicesAppBar( + isInEditMode = uiState.isInEditMode, + onEditModeChange = onEditModeChange, + scrollBehavior = scrollBehavior, + ) + }, + floatingActionButton = { + ServicesFab( + isVisible = uiState.isLoading.not(), + isExtendedVisible = uiState.services.isEmpty(), + isNormalVisible = reorderableState.listState.isScrollingUp(), + onClick = onFabClick, + ) + }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) + ) { padding -> + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TwTheme.color.background) + .padding(padding) + .reorderable(reorderableState), + state = reorderableState.listState, + contentPadding = PaddingValues(vertical = 8.dp) + ) { + if (uiState.isLoading) { + listItem(ServicesListItem.Loader) { + ServicesProgress( + Modifier + .fillParentMaxSize() + .animateItemPlacement() + ) + } + return@LazyColumn + } + + if (uiState.services.isEmpty()) { + listItem(ServicesListItem.Empty) { + ServicesEmpty( + modifier = Modifier + .fillParentMaxSize() + .animateItemPlacement(), + onExternalImportClick = onExternalImportClick + ) + } + } + + listItems( + items = uiState.services, + type = { ServicesListItem.Service(it.id) } + ) { service -> + ReorderableItem( + state = reorderableState, + key = service.id, + modifier = Modifier + .animateItemPlacement() + .animateContentSize() + ) { isDragging -> + Service( + state = service.asState(), + style = ServiceStyle.Normal, + isInEditMode = uiState.isInEditMode, + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 4.dp) + .shadow(if (isDragging) 8.dp else 0.dp) + .run { + if (uiState.isInEditMode) { + detectReorderAfterLongPress(reorderableState) + } else { + this + } + }, + onClick = { + modalType = ModalType.FocusService(service.id) + scope.launch { modalState.show() } + }, + onLongClick = { + activity.copyToClipboard( + service.code?.current.toString() + ) + }, + ) + } + } + } + } + } +} diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesUiState.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesUiState.kt new file mode 100644 index 00000000..f3522109 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesUiState.kt @@ -0,0 +1,19 @@ +package com.twofasapp.feature.home.ui.services + +import com.twofasapp.data.services.domain.Service + +data class ServicesUiState( + val services: List = emptyList(), + val isLoading: Boolean = true, + val isNextTokenEnabled: Boolean = false, + val isInEditMode: Boolean = false, + val events: List = listOf(), +) { + fun getService(id: Long): Service { + return services.first { it.id == id } + } +} + +sealed interface ServicesStateEvent { + object ShowAddServiceModal : ServicesStateEvent +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesViewModel.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesViewModel.kt new file mode 100644 index 00000000..1532396a --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/ServicesViewModel.kt @@ -0,0 +1,57 @@ +package com.twofasapp.feature.home.ui.services + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.twofasapp.data.services.GroupsRepository +import com.twofasapp.data.services.ServicesRepository +import com.twofasapp.data.services.domain.Service +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class ServicesViewModel( + private val servicesRepository: ServicesRepository, + private val groupsRepository: GroupsRepository, +) : ViewModel() { + + val uiState = MutableStateFlow(ServicesUiState()) +// val orderList = MutableStateFlow(listOf(1, 2, 3, 4, 5, 6)) + + private val isInEditMode = MutableStateFlow(false) + + init { + viewModelScope.launch { + combine( + servicesRepository.observeServicesTicker(), + isInEditMode, + ) { services, isInEditMode -> CombinedResult(services, isInEditMode) }.collect { result -> + + uiState.update { + it.copy( + services = result.services, + isLoading = false, + isInEditMode = result.isInEditMode, + ) + } + } + } + } + + fun toggleEditMode() { + isInEditMode.value = isInEditMode.value.not() + } + + fun toggleAddMenu() { + uiState.update { it.copy(events = it.events.plus(ServicesStateEvent.ShowAddServiceModal)) } + } + + fun consumeEvent(event: ServicesStateEvent) { + uiState.update { it.copy(events = it.events.minus(event)) } + } + + data class CombinedResult( + val services: List, + val isInEditMode: Boolean, + ) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/StateMapper.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/StateMapper.kt new file mode 100644 index 00000000..b9152a9e --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/StateMapper.kt @@ -0,0 +1,45 @@ +package com.twofasapp.feature.home.ui.services + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.twofasapp.data.services.domain.Service +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.service.ServiceImageType +import com.twofasapp.designsystem.service.ServiceState + +@Composable +fun Service.asState(): ServiceState { + return ServiceState( + name = name, + info = info, + code = code?.current.orEmpty(), + nextCode = code?.next.orEmpty(), + timer = code?.timer ?: 0, + progress = code?.progress ?: 0f, + imageType = when (imageType) { + Service.ImageType.IconCollection -> ServiceImageType.Icon + Service.ImageType.Label -> ServiceImageType.Label + }, + iconLight = iconLight, + iconDark = iconDark, + labelText = labelText, + labelColor = labelColor.asState(), + badgeColor = badgeColor.asState() + ) +} + +@Composable +private fun Service.Tint?.asState(): Color { + return when (this) { + Service.Tint.Default -> TwTheme.color.surfaceVariant + Service.Tint.LightBlue -> TwTheme.color.accentLightBlue + Service.Tint.Indigo -> TwTheme.color.accentIndigo + Service.Tint.Purple -> TwTheme.color.accentPurple + Service.Tint.Turquoise -> TwTheme.color.accentTurquoise + Service.Tint.Green -> TwTheme.color.accentGreen + Service.Tint.Red -> TwTheme.color.accentRed + Service.Tint.Orange -> TwTheme.color.accentOrange + Service.Tint.Yellow -> TwTheme.color.accentYellow + null -> TwTheme.color.surfaceVariant + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/AddServiceModal.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/AddServiceModal.kt new file mode 100644 index 00000000..6baa2a69 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/AddServiceModal.kt @@ -0,0 +1,17 @@ +package com.twofasapp.feature.home.ui.services.modal + +import androidx.compose.runtime.Composable +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.common.ModalList +import com.twofasapp.designsystem.settings.SettingsLink + +@Composable +internal fun AddServiceModal( + onAddManuallyClick: () -> Unit = {}, + onScanQrClick: () -> Unit = {}, +) { + ModalList { + SettingsLink(title = "Add manually", icon = TwIcons.Edit) { onAddManuallyClick() } + SettingsLink(title = "Scan QR code", icon = TwIcons.Qr) { onScanQrClick() } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/FocusServiceModal.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/FocusServiceModal.kt new file mode 100644 index 00000000..1f9c4921 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/services/modal/FocusServiceModal.kt @@ -0,0 +1,34 @@ +package com.twofasapp.feature.home.ui.services.modal + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.ModalList +import com.twofasapp.designsystem.service.Service +import com.twofasapp.designsystem.service.ServiceState +import com.twofasapp.designsystem.service.ServiceStyle +import com.twofasapp.designsystem.settings.SettingsDivider +import com.twofasapp.designsystem.settings.SettingsLink + +@Composable +internal fun FocusServiceModal( + serviceState: ServiceState, + onEditClick: () -> Unit = {}, + onCopyClick: () -> Unit = {}, +) { + Column { + Service( + state = serviceState, + style = ServiceStyle.Modal, + containerColor = TwTheme.color.surface, + ) + + SettingsDivider() + + ModalList { + SettingsLink(title = "Edit", icon = TwIcons.Edit) { onEditClick() } + SettingsLink(title = "Copy code", icon = TwIcons.Copy) { onCopyClick() } + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsScreen.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsScreen.kt new file mode 100644 index 00000000..616de82a --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsScreen.kt @@ -0,0 +1,113 @@ +package com.twofasapp.feature.home.ui.settings + +import android.app.Activity +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.designsystem.settings.SettingsDivider +import com.twofasapp.designsystem.settings.SettingsLink +import com.twofasapp.feature.home.navigation.HomeNavigationListener +import com.twofasapp.feature.home.ui.bottombar.BottomBar +import com.twofasapp.feature.home.ui.bottombar.BottomBarListener +import com.twofasapp.locale.TwLocale +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun SettingsRoute( + listener: HomeNavigationListener, + bottomBarListener: BottomBarListener, + viewModel: SettingsViewModel = koinViewModel() +) { + SettingsScreen( + listener = listener, + bottomBarListener = bottomBarListener, + ) +} + +@Composable +private fun SettingsScreen( + listener: HomeNavigationListener, + bottomBarListener: BottomBarListener, +) { + val activity = LocalContext.current as Activity + val uriHandler = LocalUriHandler.current + + Scaffold( + bottomBar = { BottomBar(1, bottomBarListener) }, + topBar = { TwTopAppBar(titleText = "Settings", showBackButton = false) }, + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TwTheme.color.background) + .padding(padding) + ) { + item { + SettingsLink(title = TwLocale.strings.settingsBackup, icon = TwIcons.CloudUpload) { + listener.openBackup(activity) + } + } + + item { + SettingsLink(title = TwLocale.strings.settingsSecurity, icon = TwIcons.Security) { + listener.openSecurity(activity) + } + } + + item { + SettingsLink(title = TwLocale.strings.settingsAppearance, icon = TwIcons.Eye) { + listener.openAppSettings() + } + } + + item { + SettingsLink(title = TwLocale.strings.settingsExternalImport, icon = TwIcons.FileUpload) { + listener.openExternalImport() + } + } + + item { + SettingsLink(title = TwLocale.strings.settingsBrowserExt, icon = TwIcons.Extension) { + listener.openBrowserExt() + } + } + + item { SettingsDivider() } + + item { + SettingsLink(title = TwLocale.strings.settingsTrash, icon = TwIcons.Delete) { + listener.openTrash() + } + } + + item { + SettingsLink(title = TwLocale.strings.settingsSupport, icon = TwIcons.Support) { + uriHandler.openUri(TwLocale.links.support) + } + } + + item { + SettingsLink(title = TwLocale.strings.settingsAbout, icon = TwIcons.Info) { + listener.openAbout() + } + } + + item { SettingsDivider() } + + item { + SettingsLink(title = TwLocale.strings.settingsDonate, icon = TwIcons.Favorite) { + uriHandler.openUri(TwLocale.links.donate) + } + } + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsViewModel.kt b/feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsViewModel.kt new file mode 100644 index 00000000..81ae7376 --- /dev/null +++ b/feature/home/src/main/java/com/twofasapp/feature/home/ui/settings/SettingsViewModel.kt @@ -0,0 +1,9 @@ +package com.twofasapp.feature.home.ui.settings + +import androidx.lifecycle.ViewModel +import com.twofasapp.common.coroutines.Dispatchers + +internal class SettingsViewModel( + private val dispatchers: Dispatchers, +) : ViewModel() { +} \ No newline at end of file diff --git a/feature/home/src/main/res/drawable-night/img_services_empty.xml b/feature/home/src/main/res/drawable-night/img_services_empty.xml new file mode 100644 index 00000000..2370e64c --- /dev/null +++ b/feature/home/src/main/res/drawable-night/img_services_empty.xml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feature/home/src/main/res/drawable/ic_android.xml b/feature/home/src/main/res/drawable/ic_android.xml deleted file mode 100644 index 11684966..00000000 --- a/feature/home/src/main/res/drawable/ic_android.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/feature/home/src/main/res/drawable/img_services_empty.xml b/feature/home/src/main/res/drawable/img_services_empty.xml new file mode 100644 index 00000000..6fe50ef7 --- /dev/null +++ b/feature/home/src/main/res/drawable/img_services_empty.xml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/notifications/src/main/res/drawable/notif_category_feature.xml b/feature/home/src/main/res/drawable/notif_category_feature.xml similarity index 100% rename from notifications/src/main/res/drawable/notif_category_feature.xml rename to feature/home/src/main/res/drawable/notif_category_feature.xml diff --git a/notifications/src/main/res/drawable/notif_category_news.xml b/feature/home/src/main/res/drawable/notif_category_news.xml similarity index 100% rename from notifications/src/main/res/drawable/notif_category_news.xml rename to feature/home/src/main/res/drawable/notif_category_news.xml diff --git a/notifications/src/main/res/drawable/notif_category_update.xml b/feature/home/src/main/res/drawable/notif_category_update.xml similarity index 100% rename from notifications/src/main/res/drawable/notif_category_update.xml rename to feature/home/src/main/res/drawable/notif_category_update.xml diff --git a/notifications/src/main/res/drawable/notif_category_video.xml b/feature/home/src/main/res/drawable/notif_category_video.xml similarity index 100% rename from notifications/src/main/res/drawable/notif_category_video.xml rename to feature/home/src/main/res/drawable/notif_category_video.xml diff --git a/feature/startup/build.gradle.kts b/feature/startup/build.gradle.kts index a3fb3166..acd2f8bf 100644 --- a/feature/startup/build.gradle.kts +++ b/feature/startup/build.gradle.kts @@ -9,7 +9,7 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":data:session")) implementation(project(":core:common")) implementation(project(":core:locale")) diff --git a/feature/startup/src/main/java/com/twofasapp/feature/startup/navigation/StartupNavigation.kt b/feature/startup/src/main/java/com/twofasapp/feature/startup/navigation/StartupNavigation.kt index be5d2107..6da8d7ee 100644 --- a/feature/startup/src/main/java/com/twofasapp/feature/startup/navigation/StartupNavigation.kt +++ b/feature/startup/src/main/java/com/twofasapp/feature/startup/navigation/StartupNavigation.kt @@ -10,9 +10,9 @@ object StartupGraph : NavGraph { } fun NavGraphBuilder.startupNavigation( - onFinish: () -> Unit + openHome: () -> Unit, ) { composable(route = StartupGraph.route) { - StartupRoute(onFinish) + StartupRoute(openHome) } } diff --git a/feature/startup/src/main/java/com/twofasapp/feature/startup/ui/StartupScreen.kt b/feature/startup/src/main/java/com/twofasapp/feature/startup/ui/StartupScreen.kt index 7a37d067..f69765ed 100644 --- a/feature/startup/src/main/java/com/twofasapp/feature/startup/ui/StartupScreen.kt +++ b/feature/startup/src/main/java/com/twofasapp/feature/startup/ui/StartupScreen.kt @@ -29,17 +29,17 @@ import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPagerIndicator import com.google.accompanist.pager.rememberPagerState -import com.twofasapp.designsystem.TwsTheme -import com.twofasapp.designsystem.composable.TwsPrimaryButton -import com.twofasapp.designsystem.composable.TwsTextButton +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwButton +import com.twofasapp.designsystem.common.TwTextButton import com.twofasapp.feature.startup.R -import com.twofasapp.locale.TwsLocale +import com.twofasapp.locale.TwLocale import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @Composable internal fun StartupRoute( - onFinish: () -> Unit, + openHome: () -> Unit, viewModel: StartupViewModel = koinViewModel() ) { StartupScreen( @@ -47,11 +47,11 @@ internal fun StartupRoute( onNextClick = { viewModel.onNextClicked(it) }, onStartUsingClick = { viewModel.onStartUsingClicked() - onFinish() + openHome() }, onSkipClick = { viewModel.onSkipClicked() - onFinish() + openHome() }, ) } @@ -88,49 +88,49 @@ internal fun StartupScreen( when (page) { 0 -> Step( image = painterResource(id = R.drawable.onboarding_step_one), - headerText = TwsLocale.strings.startupStepOneHeader, - bodyText = TwsLocale.strings.startupStepOneBody, + headerText = TwLocale.strings.startupStepOneHeader, + bodyText = TwLocale.strings.startupStepOneBody, imageSize = 60.dp, ) 1 -> Step( image = painterResource(id = R.drawable.onboarding_step_two), - headerText = TwsLocale.strings.startupStepTwoHeader, - bodyText = TwsLocale.strings.startupStepTwoBody, + headerText = TwLocale.strings.startupStepTwoHeader, + bodyText = TwLocale.strings.startupStepTwoBody, ) 2 -> Step( image = painterResource(id = R.drawable.onboarding_step_three), - headerText = TwsLocale.strings.startupStepThreeHeader, - bodyText = TwsLocale.strings.startupStepThreeBody, + headerText = TwLocale.strings.startupStepThreeHeader, + bodyText = TwLocale.strings.startupStepThreeBody, ) 3 -> Step( image = painterResource(id = R.drawable.onboarding_step_four), - headerText = TwsLocale.strings.startupStepFourHeader, - bodyText = TwsLocale.strings.startupStepFourBody, + headerText = TwLocale.strings.startupStepFourHeader, + bodyText = TwLocale.strings.startupStepFourBody, ) } } if (pagerState.currentPage == 0) { Text( - text = TwsLocale.strings.startupTermsLabel, + text = TwLocale.strings.startupTermsLabel, style = MaterialTheme.typography.bodyMedium, - color = TwsTheme.color.onSurfaceDarker, + color = TwTheme.color.onSurfaceSecondary, modifier = Modifier - .clip(TwsTheme.shape.roundedDefault) + .clip(TwTheme.shape.roundedDefault) .clickable { onTermsClick() - uriHandler.openUri(TwsLocale.links.terms) + uriHandler.openUri(TwLocale.links.terms) } .padding(4.dp) ) } else { HorizontalPagerIndicator( pagerState = pagerState, - activeColor = TwsTheme.color.primary, - inactiveColor = TwsTheme.color.divider, + activeColor = TwTheme.color.primary, + inactiveColor = TwTheme.color.divider, pageCount = pagerState.pageCount - 1, pageIndexMapping = { it - 1 } ) @@ -138,36 +138,37 @@ internal fun StartupScreen( Spacer(modifier = Modifier.height(12.dp)) - TwsPrimaryButton( + TwButton( text = when (pagerState.currentPage) { - 1 -> TwsLocale.strings.commonNext - 2 -> TwsLocale.strings.commonNext - 3 -> TwsLocale.strings.startupStartCta - else -> TwsLocale.strings.commonContinue + 1 -> TwLocale.strings.commonNext + 2 -> TwLocale.strings.commonNext + 3 -> TwLocale.strings.startupStartCta + else -> TwLocale.strings.commonContinue }, modifier = Modifier.padding(vertical = 24.dp), - ) { - if (pagerState.currentPage == pagerState.pageCount - 1) { - onStartUsingClick() - } else { - onNextClick(pagerState.currentPage) + onClick = { + if (pagerState.currentPage == pagerState.pageCount - 1) { + onStartUsingClick() + } else { + onNextClick(pagerState.currentPage) + } + + scope.launch { + pagerState.animateScrollToPage(page = pagerState.currentPage + 1) + } } - scope.launch { - pagerState.animateScrollToPage(page = pagerState.currentPage + 1) - } - } + ) } if (pagerState.currentPage != 0) { - TwsTextButton( - text = TwsLocale.strings.commonSkip, + TwTextButton( + text = TwLocale.strings.commonSkip, modifier = Modifier .align(Alignment.TopEnd) - .padding(8.dp) - ) { - onSkipClick() - } + .padding(8.dp), + onClick = { onSkipClick() } + ) } } } diff --git a/feature/trash/.gitignore b/feature/trash/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/trash/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/trash/build.gradle.kts b/feature/trash/build.gradle.kts new file mode 100644 index 00000000..a15de3ba --- /dev/null +++ b/feature/trash/build.gradle.kts @@ -0,0 +1,21 @@ +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + alias(libs.plugins.twofasAndroidLibrary) + alias(libs.plugins.twofasCompose) +} + +android { + namespace = "com.twofasapp.feature.trash" +} + +dependencies { + implementation(project(":core:di")) + implementation(project(":core:common")) + implementation(project(":core:locale")) + implementation(project(":core:designsystem")) + implementation(project(":data:services")) + + implementation(libs.bundles.compose) + implementation(libs.bundles.viewModel) + implementation(libs.bundles.accompanist) +} \ No newline at end of file diff --git a/feature/trash/src/main/AndroidManifest.xml b/feature/trash/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/trash/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/trash/src/main/java/com/twofasapp/feature/trash/di/TrashModule.kt b/feature/trash/src/main/java/com/twofasapp/feature/trash/di/TrashModule.kt new file mode 100644 index 00000000..24744c08 --- /dev/null +++ b/feature/trash/src/main/java/com/twofasapp/feature/trash/di/TrashModule.kt @@ -0,0 +1,14 @@ +package com.twofasapp.feature.trash.di + +import com.twofasapp.di.KoinModule +import com.twofasapp.feature.trash.ui.dispose.DisposeViewModel +import com.twofasapp.feature.trash.ui.trash.TrashViewModel +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.dsl.module + +class TrashModule : KoinModule { + override fun provide() = module { + viewModelOf(::TrashViewModel) + viewModelOf(::DisposeViewModel) + } +} \ No newline at end of file diff --git a/feature/trash/src/main/java/com/twofasapp/feature/trash/navigation/TrashNavigation.kt b/feature/trash/src/main/java/com/twofasapp/feature/trash/navigation/TrashNavigation.kt new file mode 100644 index 00000000..4fa1fab6 --- /dev/null +++ b/feature/trash/src/main/java/com/twofasapp/feature/trash/navigation/TrashNavigation.kt @@ -0,0 +1,50 @@ +package com.twofasapp.feature.trash.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import androidx.navigation.navigation +import com.twofasapp.common.navigation.NavGraph +import com.twofasapp.common.navigation.NavNode +import com.twofasapp.common.navigation.withArg +import com.twofasapp.feature.trash.ui.dispose.DisposeRoute +import com.twofasapp.feature.trash.ui.trash.TrashRoute + +object TrashGraph : NavGraph { + override val route: String = "trash" +} + +internal object NavArg { + val ServiceId = navArgument("id") { type = NavType.LongType } +} + +private sealed class Node(override val path: String) : NavNode { + override val graph: NavGraph = TrashGraph + + object Main : Node("main") + object Dispose : Node("dispose/{${NavArg.ServiceId.name}}") +} + +fun NavGraphBuilder.trashNavigation( + navController: NavHostController, +) { + navigation( + route = TrashGraph.route, + startDestination = Node.Main.route, + ) { + composable(Node.Main.route) { + TrashRoute( + openDispose = { navController.navigate(Node.Dispose.route.withArg(NavArg.ServiceId, it)) } + ) + } + + composable( + route = Node.Dispose.route, + arguments = listOf(NavArg.ServiceId) + ) { + DisposeRoute(navigateBack = { navController.popBackStack() }) + } + } +} \ No newline at end of file diff --git a/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeScreen.kt b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeScreen.kt new file mode 100644 index 00000000..43e22820 --- /dev/null +++ b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeScreen.kt @@ -0,0 +1,40 @@ +package com.twofasapp.feature.trash.ui.dispose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.twofasapp.designsystem.common.TwButton +import com.twofasapp.designsystem.common.TwOutlinedButton +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.locale.TwLocale +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun DisposeRoute( + navigateBack: () -> Unit, + viewModel: DisposeViewModel = koinViewModel() +) { + DisposeScreen( + onDeleteClick = { + viewModel.delete() + navigateBack() + }, + onFinish = navigateBack + ) +} + +@Composable +private fun DisposeScreen( + onDeleteClick: () -> Unit, + onFinish: () -> Unit, +) { + + Scaffold(topBar = { TwTopAppBar(TwLocale.strings.trashTitle) }) { padding -> + Column(Modifier.padding(padding)) { + TwButton(text = "Delete forever", onClick = onDeleteClick) + TwOutlinedButton(text = "Cancel", onClick = onFinish) + } + } +} \ No newline at end of file diff --git a/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeViewModel.kt b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeViewModel.kt new file mode 100644 index 00000000..cc3232c1 --- /dev/null +++ b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/dispose/DisposeViewModel.kt @@ -0,0 +1,21 @@ +package com.twofasapp.feature.trash.ui.dispose + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.twofasapp.common.coroutines.Dispatchers +import com.twofasapp.common.navigation.errorNavArg +import com.twofasapp.data.services.ServicesRepository +import com.twofasapp.feature.trash.navigation.NavArg + +class DisposeViewModel( + savedStateHandle: SavedStateHandle, + private val dispatchers: Dispatchers, + private val servicesRepository: ServicesRepository, +) : ViewModel() { + + private val serviceId: Long = savedStateHandle[NavArg.ServiceId.name] ?: errorNavArg(NavArg.ServiceId) + + fun delete() { + // See: DeleteServiceUseCase + } +} \ No newline at end of file diff --git a/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashScreen.kt b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashScreen.kt new file mode 100644 index 00000000..fec600d3 --- /dev/null +++ b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashScreen.kt @@ -0,0 +1,136 @@ +package com.twofasapp.feature.trash.ui.trash + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twofasapp.data.services.domain.Service +import com.twofasapp.designsystem.TwIcons +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwDropdownMenu +import com.twofasapp.designsystem.common.TwDropdownMenuItem +import com.twofasapp.designsystem.common.TwIconButton +import com.twofasapp.designsystem.common.TwTopAppBar +import com.twofasapp.designsystem.service.ServiceImageType +import com.twofasapp.designsystem.service.ServiceNoCode +import com.twofasapp.locale.TwLocale +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun TrashRoute( + openDispose: (Long) -> Unit, + viewModel: TrashViewModel = koinViewModel() +) { + val services by viewModel.services.collectAsStateWithLifecycle() + + TrashScreen( + services = services, + onRestoreClick = { viewModel.restoreService(it) }, + onDisposeClick = { openDispose(it) }, + ) +} + +@Composable +private fun TrashScreen( + services: List, + onRestoreClick: (Long) -> Unit, + onDisposeClick: (Long) -> Unit, +) { + + Scaffold(topBar = { TwTopAppBar(TwLocale.strings.trashTitle) }) { padding -> + + LazyColumn(Modifier.padding(padding)) { + + if (services.isEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp) + ) { + Text( + text = TwLocale.strings.trashEmpty, + style = TwTheme.typo.body3, + color = TwTheme.color.onSurfacePrimary, + modifier = Modifier.align(Alignment.Center) + ) + } + } + + return@LazyColumn + } + + items(services, key = { it.id }) { + ServiceNoCode( + name = it.name, + info = it.info, + imageType = when (it.imageType) { + Service.ImageType.IconCollection -> ServiceImageType.Icon + Service.ImageType.Label -> ServiceImageType.Label + }, + iconLight = it.iconLight, + iconDark = it.iconDark, + labelText = it.labelText, + labelColor = it.badgeColor.asState(), + imageSize = 32.dp, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 0.dp), + endContent = { + var expanded by rememberSaveable { mutableStateOf(false) } + + TwDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + anchor = { TwIconButton(painter = TwIcons.More, onClick = { expanded = true }) } + ) { + TwDropdownMenuItem( + text = TwLocale.strings.trashRestoreCta, + onClick = { + expanded = false + onRestoreClick(it.id) + } + ) + TwDropdownMenuItem( + text = TwLocale.strings.trashDisposeCta, + onClick = { + expanded = false + onDisposeClick(it.id) + } + ) + } + } + ) + } + } + } +} + +@Composable +private fun Service.Tint?.asState(): Color { + return when (this) { + Service.Tint.Default -> TwTheme.color.surfaceVariant + Service.Tint.LightBlue -> TwTheme.color.accentLightBlue + Service.Tint.Indigo -> TwTheme.color.accentIndigo + Service.Tint.Purple -> TwTheme.color.accentPurple + Service.Tint.Turquoise -> TwTheme.color.accentTurquoise + Service.Tint.Green -> TwTheme.color.accentGreen + Service.Tint.Red -> TwTheme.color.accentRed + Service.Tint.Orange -> TwTheme.color.accentOrange + Service.Tint.Yellow -> TwTheme.color.accentYellow + null -> TwTheme.color.surfaceVariant + } +} \ No newline at end of file diff --git a/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashViewModel.kt b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashViewModel.kt new file mode 100644 index 00000000..06e236a1 --- /dev/null +++ b/feature/trash/src/main/java/com/twofasapp/feature/trash/ui/trash/TrashViewModel.kt @@ -0,0 +1,33 @@ +package com.twofasapp.feature.trash.ui.trash + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.twofasapp.common.coroutines.Dispatchers +import com.twofasapp.data.services.ServicesRepository +import com.twofasapp.data.services.domain.Service +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class TrashViewModel( + private val dispatchers: Dispatchers, + private val servicesRepository: ServicesRepository, +) : ViewModel() { + + val services: StateFlow> = + servicesRepository.observeDeletedServices() + .map { list -> list.sortedByDescending { it.id } } // TODO: Sort by time + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + + fun restoreService(id: Long) { + viewModelScope.launch(dispatchers.io) { + servicesRepository.restoreService(id) + } + } +} \ No newline at end of file diff --git a/featuretoggle/build.gradle.kts b/featuretoggle/build.gradle.kts index 8c0a8dfb..38ebe88d 100644 --- a/featuretoggle/build.gradle.kts +++ b/featuretoggle/build.gradle.kts @@ -9,9 +9,10 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) - implementation(project(":environment")) + implementation(project(":core:di")) + implementation(project(":prefs")) + implementation(project(":core:common")) implementation(libs.bundles.appCompat) implementation(libs.bundles.compose) diff --git a/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/IsFeatureEnabledCaseImpl.kt b/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/IsFeatureEnabledCaseImpl.kt index 8102523e..81c5d0ac 100644 --- a/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/IsFeatureEnabledCaseImpl.kt +++ b/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/IsFeatureEnabledCaseImpl.kt @@ -1,17 +1,17 @@ package com.twofasapp.featuretoggle.domain -import com.twofasapp.environment.AppConfig -import com.twofasapp.environment.BuildVariant +import com.twofasapp.common.environment.AppBuild +import com.twofasapp.common.environment.BuildVariant import com.twofasapp.featuretoggle.domain.model.FeatureToggle import com.twofasapp.featuretoggle.domain.repository.FeatureToggleRepository internal class IsFeatureEnabledCaseImpl( - private val appConfig: AppConfig, + private val appBuild: AppBuild, private val featureToggleRepository: FeatureToggleRepository, ) : IsFeatureEnabledCase { override fun execute(featureToggle: FeatureToggle): Boolean { - if (appConfig.buildVariant == BuildVariant.Release) { + if (appBuild.buildVariant == BuildVariant.Release) { return featureToggle.default } diff --git a/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/repository/RemoteConfigRepositoryImpl.kt b/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/repository/RemoteConfigRepositoryImpl.kt index a818e5a2..5a3dbed5 100644 --- a/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/repository/RemoteConfigRepositoryImpl.kt +++ b/featuretoggle/src/main/java/com/twofasapp/featuretoggle/domain/repository/RemoteConfigRepositoryImpl.kt @@ -4,14 +4,14 @@ import com.google.firebase.ktx.Firebase import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.ktx.remoteConfig import com.google.firebase.remoteconfig.ktx.remoteConfigSettings -import com.twofasapp.environment.AppConfig +import com.twofasapp.common.environment.AppBuild import com.twofasapp.featuretoggle.R import com.twofasapp.featuretoggle.domain.model.RemoteConfig import io.reactivex.Flowable import io.reactivex.processors.BehaviorProcessor internal class RemoteConfigRepositoryImpl( - private val appConfig: AppConfig, + private val appBuild: AppBuild, ) : RemoteConfigRepository { private val remoteConfig: FirebaseRemoteConfig? @@ -29,7 +29,7 @@ internal class RemoteConfigRepositoryImpl( override fun fetchAndActivate() { remoteConfig?.setConfigSettingsAsync( remoteConfigSettings { - minimumFetchIntervalInSeconds = if (appConfig.isDebug) 0 else 3600 + minimumFetchIntervalInSeconds = if (appBuild.isDebuggable) 0 else 3600 } ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a34a55cd..08d367b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,9 +10,9 @@ espresso = "3.5.0" koin = "3.2.2" koinAndroid = "3.3.0" kotest = "5.5.2" -kotlin = "1.7.20" +kotlin = "1.8.0" kotlinCoroutines = "1.6.4" -kotlinKsp = "1.7.20-1.0.6" +kotlinKsp = "1.8.0-1.0.8" ktlint = "3.12.0" ktor = "2.1.2" material3 = "1.0.1" @@ -22,7 +22,6 @@ viewModel = "2.6.0-alpha03" [libraries] appcompat = "androidx.appcompat:appcompat:1.4.1" accompanistFlowLayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } -accompanistNavigationAnimation = { module = "com.google.accompanist:accompanist-navigation-animation", version.ref = "accompanist" } accompanistPlaceholder = { module = "com.google.accompanist:accompanist-placeholder", version.ref = "accompanist" } accompanistSystemUi = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanistPager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } @@ -34,6 +33,7 @@ composeAnimation = { module = "androidx.compose.animation:animation", version.re composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composeCompiler" } composeMaterial2 = { module = "androidx.compose.material:material", version.ref = "compose" } composeUi = { module = "androidx.compose.ui:ui", version.ref = "compose" } +composeUiUtil = { module = "androidx.compose.ui:ui-util", version.ref = "compose" } composeUiTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } composeConstraint = "androidx.constraintlayout:constraintlayout-compose:1.0.0-beta02" core = { module = "androidx.core:core-ktx", version.ref = "core" } @@ -49,7 +49,7 @@ koinTestJunit = { module = "io.insert-koin:koin-test-junit4", version.ref = "koi kotest = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotlinCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinCoroutines" } kotlinCoroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinCoroutines" } -kotlinSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0" +kotlinSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" kotlinTestJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } ktorAuth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } ktorContentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } @@ -111,6 +111,7 @@ securityCrypto = "androidx.security:security-crypto:1.1.0-alpha03" secureStorage = "de.adorsys.android:securestoragelibrary:1.2.4" roomRx = { module = "androidx.room:room-rxjava2", version.ref = "room" } sqlCipher = "net.zetetic:android-database-sqlcipher:4.5.2" +apacheCommonsCodec = "commons-codec:commons-codec:1.15" [bundles] appCompat = [ @@ -148,7 +149,6 @@ barcodeScanner = [ ] accompanist = [ "accompanistFlowLayout", - "accompanistNavigationAnimation", "accompanistPlaceholder", "accompanistSystemUi", "accompanistPager", @@ -162,6 +162,7 @@ compose = [ "composeActivity", "composeMaterial2", "composeUi", + "composeUiUtil", "composeUiTooling", "material3", "material3Window", diff --git a/navigation/src/main/java/com/twofasapp/navigation/ExternalImportDirections.kt b/navigation/src/main/java/com/twofasapp/navigation/ExternalImportDirections.kt deleted file mode 100644 index 266bffc1..00000000 --- a/navigation/src/main/java/com/twofasapp/navigation/ExternalImportDirections.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.twofasapp.navigation - -import com.twofasapp.navigation.base.Directions - -sealed interface ExternalImportDirections : Directions { - object GoBack : ExternalImportDirections - object Main : ExternalImportDirections - data class ImportScan(val startWithGallery: Boolean = false) : ExternalImportDirections - data class ImportResult(val type: Type, val content: String) : ExternalImportDirections { - enum class Type { GoogleAuthenticator, Aegis, Raivo } - } - - object GoogleAuthenticator : ExternalImportDirections - object Aegis : ExternalImportDirections - object Raivo : ExternalImportDirections -} \ No newline at end of file diff --git a/navigation/src/main/java/com/twofasapp/navigation/ExternalImportRouter.kt b/navigation/src/main/java/com/twofasapp/navigation/ExternalImportRouter.kt deleted file mode 100644 index 8292c61f..00000000 --- a/navigation/src/main/java/com/twofasapp/navigation/ExternalImportRouter.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.twofasapp.navigation - -import com.twofasapp.navigation.base.Router - -abstract class ExternalImportRouter : Router() \ No newline at end of file diff --git a/navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt b/navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt deleted file mode 100644 index f09e66f9..00000000 --- a/navigation/src/main/java/com/twofasapp/navigation/SettingsDirections.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.twofasapp.navigation - -import com.twofasapp.navigation.base.Directions - -sealed interface SettingsDirections : Directions { - object GoBack : SettingsDirections - object Main : SettingsDirections - object Theme : SettingsDirections - object BrowserExtension : SettingsDirections - class BrowserDetails(val extensionId: String) : SettingsDirections - object PairingScan : SettingsDirections - class PairingProgress(val extensionId: String) : SettingsDirections -} \ No newline at end of file diff --git a/navigation/src/main/java/com/twofasapp/navigation/SettingsRouter.kt b/navigation/src/main/java/com/twofasapp/navigation/SettingsRouter.kt deleted file mode 100644 index 8640da8c..00000000 --- a/navigation/src/main/java/com/twofasapp/navigation/SettingsRouter.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.twofasapp.navigation - -import com.twofasapp.navigation.base.Router - -abstract class SettingsRouter : Router() \ No newline at end of file diff --git a/navigation/src/main/java/com/twofasapp/navigation/StartDirections.kt b/navigation/src/main/java/com/twofasapp/navigation/StartDirections.kt index 346df2b4..f3bcc7ea 100644 --- a/navigation/src/main/java/com/twofasapp/navigation/StartDirections.kt +++ b/navigation/src/main/java/com/twofasapp/navigation/StartDirections.kt @@ -3,6 +3,5 @@ package com.twofasapp.navigation import com.twofasapp.navigation.base.Directions sealed interface StartDirections : Directions { - object Onboarding : StartDirections object Main : StartDirections } \ No newline at end of file diff --git a/network/src/main/java/com/twofasapp/network/NetworkModule.kt b/network/src/main/java/com/twofasapp/network/NetworkModule.kt deleted file mode 100644 index 4d3f096f..00000000 --- a/network/src/main/java/com/twofasapp/network/NetworkModule.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.twofasapp.network - -import com.twofasapp.di.KoinModule -import com.twofasapp.environment.AppConfig -import com.twofasapp.network.api.BrowserExtensionApi -import com.twofasapp.network.api.NotificationsApi -import com.twofasapp.serialization.JsonSerializer -import io.ktor.client.* -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module -import timber.log.Timber - -class NetworkModule : KoinModule { - - override fun provide() = module { - -// single { -// OkHttpClient.Builder() -// .sslSocketFactory(SslSocketFactory.create(), TrustManagerSelfSigned()) -// .hostnameVerifier(HostVerifier()) -// .addInterceptor(get()) -// .connectTimeout(60, TimeUnit.SECONDS) -// .readTimeout(60, TimeUnit.SECONDS) -// .writeTimeout(60, TimeUnit.SECONDS) -// .build() -// } - - singleOf(::BrowserExtensionApi) - singleOf(::NotificationsApi) - - single { - val isDebug = get().isDebug - - HttpClient(OkHttp) { - expectSuccess = true - - if (isDebug) { - install(Logging) { - logger = object : Logger { - override fun log(message: String) { - Timber.tag("Ktor").v(message) - } - } - level = LogLevel.ALL - } - } - - install(ContentNegotiation) { - json(get().json) - } - install(DefaultRequest) { - contentType(ContentType.Application.Json) - } - } - } - } -} \ No newline at end of file diff --git a/network/src/main/java/com/twofasapp/network/api/BrowserExtensionApi.kt b/network/src/main/java/com/twofasapp/network/api/BrowserExtensionApi.kt deleted file mode 100644 index b5af01af..00000000 --- a/network/src/main/java/com/twofasapp/network/api/BrowserExtensionApi.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.twofasapp.network.api - -import com.twofasapp.network.body.ApproveLoginRequestBody -import com.twofasapp.network.body.DenyLoginRequestBody -import com.twofasapp.network.body.DeviceRegisterBody -import com.twofasapp.network.body.PairBrowserBody -import com.twofasapp.network.exception.BrowserAlreadyPairedException -import com.twofasapp.network.response.BrowserResponse -import com.twofasapp.network.response.DeviceRegisterResponse -import com.twofasapp.network.response.PairBrowserResponse -import com.twofasapp.network.response.TokenRequestResponse -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject - -class BrowserExtensionApi( - private val client: HttpClient, -) { - - companion object { - private const val baseUrl = "https://api2.2fas.com" - } - - suspend fun registerMobileDevice(body: DeviceRegisterBody): DeviceRegisterResponse { - return client.post("$baseUrl/mobile/devices") { setBody(body) }.body() - } - - suspend fun updateMobileDevice(deviceId: String, newName: String) { - client.put("$baseUrl/mobile/devices/$deviceId") { - setBody(buildJsonObject { put("name", JsonPrimitive(newName)) }) - }.body() - } - - suspend fun pairBrowser(deviceId: String, body: PairBrowserBody): PairBrowserResponse { - return try { - client.post("$baseUrl/mobile/devices/$deviceId/browser_extensions") { setBody(body) }.body() - } catch (e: Exception) { - throw when { - e is ClientRequestException && e.response.status.value == 409 -> BrowserAlreadyPairedException() - else -> e - } - } - } - - suspend fun updateBrowserName(extensionId: String, newName: String) { - client.put("$baseUrl/browser_extensions/$extensionId") { - setBody(buildJsonObject { put("name", JsonPrimitive(newName)) }) - }.body() - } - - suspend fun deletePairedBrowser(deviceId: String, extensionId: String) { - return client.delete("$baseUrl/mobile/devices/$deviceId/browser_extensions/${extensionId}").body() - } - - suspend fun getBrowser(deviceId: String, extensionId: String): BrowserResponse { - return client.get("$baseUrl/mobile/devices/$deviceId/browser_extensions/${extensionId}").body() - } - - suspend fun getBrowsers(deviceId: String): List { - return client.get("$baseUrl/mobile/devices/$deviceId/browser_extensions").body() - } - - suspend fun acceptLoginRequest(deviceId: String, body: ApproveLoginRequestBody) { - client.post("$baseUrl/mobile/devices/$deviceId/commands/send_2fa_token") { setBody(body) }.body() - } - - suspend fun denyLoginRequest(extensionId: String, tokenRequestId: String) { - client.post("$baseUrl/browser_extensions/$extensionId/2fa_requests/$tokenRequestId/commands/close_2fa_request") { setBody(DenyLoginRequestBody()) } - .body() - } - - suspend fun fetchTokenRequests(deviceId: String): List { - return client.get("$baseUrl/mobile/devices/$deviceId/browser_extensions/2fa_requests").body() - } -} diff --git a/network/src/main/java/com/twofasapp/network/api/NotificationsApi.kt b/network/src/main/java/com/twofasapp/network/api/NotificationsApi.kt deleted file mode 100644 index 680e2ca8..00000000 --- a/network/src/main/java/com/twofasapp/network/api/NotificationsApi.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.twofasapp.network.api - -import com.twofasapp.network.response.NotificationResponse -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter - -class NotificationsApi( - private val client: HttpClient, -) { - - companion object { - private const val baseUrl = "https://api2.2fas.com" - } - - suspend fun fetchNotifications(publishedAfter: OffsetDateTime): List = - client.get("$baseUrl/mobile/notifications") { - parameter("platform", "android") - parameter("published_after", publishedAfter.format(DateTimeFormatter.ISO_INSTANT)) - }.body() -} diff --git a/network/src/main/java/com/twofasapp/network/config/HostVerifier.kt b/network/src/main/java/com/twofasapp/network/config/HostVerifier.kt deleted file mode 100644 index 6787c4cd..00000000 --- a/network/src/main/java/com/twofasapp/network/config/HostVerifier.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.twofasapp.network.config - -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.SSLSession - -class HostVerifier(private val host: String? = null, private val trustByDefault: Boolean = true) : HostnameVerifier { - override fun verify(hostname: String, session: SSLSession): Boolean { - if (host == null) { - return trustByDefault - } else { - return host.lowercase().trim().contains(hostname.lowercase().trim()) - } - } -} \ No newline at end of file diff --git a/network/src/main/java/com/twofasapp/network/config/SslSocketFactory.kt b/network/src/main/java/com/twofasapp/network/config/SslSocketFactory.kt deleted file mode 100644 index 4960c5dd..00000000 --- a/network/src/main/java/com/twofasapp/network/config/SslSocketFactory.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.twofasapp.network.config - -import java.security.SecureRandom -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLSocketFactory -import javax.net.ssl.TrustManager - -class SslSocketFactory { - companion object { - fun create(protocol: String = "SSL", trustManager: TrustManager = TrustManagerSelfSigned()): SSLSocketFactory { - val sslContext = SSLContext.getInstance(protocol) - sslContext.init(null, arrayOf(trustManager), SecureRandom()) - return sslContext.socketFactory - } - } -} \ No newline at end of file diff --git a/network/src/main/java/com/twofasapp/network/config/TrustManagerSelfSigned.kt b/network/src/main/java/com/twofasapp/network/config/TrustManagerSelfSigned.kt deleted file mode 100644 index aed68b1f..00000000 --- a/network/src/main/java/com/twofasapp/network/config/TrustManagerSelfSigned.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.twofasapp.network.config - -import java.security.cert.X509Certificate -import javax.net.ssl.X509TrustManager - -class TrustManagerSelfSigned : X509TrustManager { - override fun checkClientTrusted(chain: Array, authType: String) = Unit - override fun checkServerTrusted(chain: Array, authType: String) = Unit - override fun getAcceptedIssuers(): Array = arrayOf() -} \ No newline at end of file diff --git a/notifications/build.gradle.kts b/notifications/build.gradle.kts deleted file mode 100644 index 10a306f9..00000000 --- a/notifications/build.gradle.kts +++ /dev/null @@ -1,29 +0,0 @@ -@Suppress("DSL_SCOPE_VIOLATION") -plugins { - alias(libs.plugins.twofasAndroidLibrary) - alias(libs.plugins.twofasCompose) -} - -android { - namespace = "com.twofasapp.notifications" -} - -dependencies { - implementation(project(":base")) - implementation(project(":core")) - implementation(project(":di")) - implementation(project(":design")) - implementation(project(":resources")) - implementation(project(":extensions")) - implementation(project(":persistence")) - implementation(project(":prefs")) - implementation(project(":network")) - implementation(project(":environment")) - implementation(project(":time:domain")) - - implementation(libs.bundles.appCompat) - implementation(libs.kotlinCoroutines) - implementation(libs.bundles.compose) - implementation(libs.bundles.viewModel) - implementation(libs.timber) -} diff --git a/notifications/src/main/java/com/twofasapp/notifications/NotificationsModule.kt b/notifications/src/main/java/com/twofasapp/notifications/NotificationsModule.kt deleted file mode 100644 index f0af41f4..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/NotificationsModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.twofasapp.notifications - -import com.twofasapp.di.KoinModule -import com.twofasapp.notifications.data.NotificationsLocalData -import com.twofasapp.notifications.data.NotificationsLocalDataImpl -import com.twofasapp.notifications.data.NotificationsRemoteData -import com.twofasapp.notifications.data.NotificationsRemoteDataImpl -import com.twofasapp.notifications.domain.* -import com.twofasapp.notifications.domain.repository.NotificationsRepository -import com.twofasapp.notifications.domain.repository.NotificationsRepositoryImpl -import com.twofasapp.notifications.ui.NotificationsViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.bind -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -class NotificationsModule : KoinModule { - - override fun provide() = module { - singleOf(::NotificationsLocalDataImpl) { bind() } - singleOf(::NotificationsRemoteDataImpl) { bind() } - singleOf(::NotificationsRepositoryImpl) { bind() } - - singleOf(::FetchNotificationsCaseImpl) { bind() } - singleOf(::ObserveNotificationsCase) - singleOf(::GetNotificationsCase) - singleOf(::ReadAllNotificationsCase) - singleOf(::HasUnreadNotificationsCaseImpl) { bind() } - - viewModelOf(::NotificationsViewModel) - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalData.kt b/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalData.kt deleted file mode 100644 index e616bdb0..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalData.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.twofasapp.notifications.data - -import com.twofasapp.notifications.domain.model.Notification -import kotlinx.coroutines.flow.Flow - -internal interface NotificationsLocalData { - suspend fun getNotifications(): List - fun observeNotifications(): Flow> - suspend fun saveNotifications(notifications: List) - suspend fun deleteNotifications(ids: List) - suspend fun readAllNotifications() -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalDataImpl.kt b/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalDataImpl.kt deleted file mode 100644 index 597c6b04..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsLocalDataImpl.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.twofasapp.notifications.data - -import com.twofasapp.notifications.domain.converter.toDomain -import com.twofasapp.notifications.domain.converter.toEntity -import com.twofasapp.notifications.domain.model.Notification -import com.twofasapp.persistence.dao.NotificationDao -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -internal class NotificationsLocalDataImpl( - private val notificationDao: NotificationDao, -) : NotificationsLocalData { - - override suspend fun getNotifications(): List { - return notificationDao.select().map { it.toDomain() } - } - - override fun observeNotifications(): Flow> { - return notificationDao.observe().map { list -> - list.map { it.toDomain() } - } - } - - override suspend fun saveNotifications(notifications: List) { - notificationDao.upsert(notifications.map { it.toEntity() }) - } - - override suspend fun deleteNotifications(ids: List) { - notificationDao.delete(ids) - } - - override suspend fun readAllNotifications() { - notificationDao.update( - *notificationDao.select().map { it.copy(isRead = true) }.toTypedArray() - ) - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteData.kt b/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteData.kt deleted file mode 100644 index 0362f2f9..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteData.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.twofasapp.notifications.data - -import com.twofasapp.notifications.domain.model.Notification -import java.time.LocalDateTime -import java.time.OffsetDateTime - -internal interface NotificationsRemoteData { - suspend fun fetchNotifications(publishedAfter: OffsetDateTime): List -} diff --git a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteDataImpl.kt b/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteDataImpl.kt deleted file mode 100644 index 9f6a86ca..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/data/NotificationsRemoteDataImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.twofasapp.notifications.data - -import com.twofasapp.network.api.NotificationsApi -import com.twofasapp.notifications.domain.converter.toDomain -import com.twofasapp.notifications.domain.model.Notification -import java.time.OffsetDateTime - -internal class NotificationsRemoteDataImpl( - private val notificationsApi: NotificationsApi -) : NotificationsRemoteData { - - override suspend fun fetchNotifications(publishedAfter: OffsetDateTime): List { - return notificationsApi.fetchNotifications(publishedAfter) - .filter { it.published_at.isNotBlank() } - .map { it.toDomain() } - } -} diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCase.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCase.kt deleted file mode 100644 index 4ce8448e..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCase.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.twofasapp.notifications.domain - -interface FetchNotificationsCase { - suspend operator fun invoke() -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCaseImpl.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCaseImpl.kt deleted file mode 100644 index bbe96485..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/FetchNotificationsCaseImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.twofasapp.notifications.domain - -import com.twofasapp.notifications.domain.repository.NotificationsRepository -import timber.log.Timber - -internal class FetchNotificationsCaseImpl( - private val notificationsRepository: NotificationsRepository, -) : FetchNotificationsCase { - - override suspend operator fun invoke() { - return try { - notificationsRepository.fetchNotifications() - } catch (e: Exception) { - Timber.e(e) - } - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/GetNotificationsCase.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/GetNotificationsCase.kt deleted file mode 100644 index 37585e27..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/GetNotificationsCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.twofasapp.notifications.domain - -import com.twofasapp.notifications.domain.model.Notification -import com.twofasapp.notifications.domain.repository.NotificationsRepository -import com.twofasapp.notifications.domain.repository.NotificationsRepository.Companion.publishedAfterDays -import com.twofasapp.time.domain.TimeProvider -import java.time.Duration - -internal class GetNotificationsCase( - private val notificationsRepository: NotificationsRepository, - private val timeProvider: TimeProvider, -) { - - suspend operator fun invoke(): List { - return notificationsRepository.getNotifications() - .filter { it.publishTime > timeProvider.systemCurrentTime() - Duration.ofDays(publishedAfterDays).toMillis() } - .sortedWith(compareBy({ it.isRead }, { it.publishTime.unaryMinus() })) - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCase.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCase.kt deleted file mode 100644 index aa93e2de..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCase.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.twofasapp.notifications.domain - -import kotlinx.coroutines.flow.Flow - -interface HasUnreadNotificationsCase { - operator fun invoke(): Flow -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCaseImpl.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCaseImpl.kt deleted file mode 100644 index c584cd49..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/HasUnreadNotificationsCaseImpl.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.twofasapp.notifications.domain - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -internal class HasUnreadNotificationsCaseImpl( - private val observeNotificationsCase: ObserveNotificationsCase, -) : HasUnreadNotificationsCase { - - override operator fun invoke(): Flow { - return observeNotificationsCase().map { list -> - list.any { it.isRead.not() } - } - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/ObserveNotificationsCase.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/ObserveNotificationsCase.kt deleted file mode 100644 index e977afbe..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/ObserveNotificationsCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.twofasapp.notifications.domain - -import com.twofasapp.notifications.domain.model.Notification -import com.twofasapp.notifications.domain.repository.NotificationsRepository -import com.twofasapp.notifications.domain.repository.NotificationsRepository.Companion.publishedAfterDays -import com.twofasapp.time.domain.TimeProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import java.time.Duration - -internal class ObserveNotificationsCase( - private val notificationsRepository: NotificationsRepository, - private val timeProvider: TimeProvider, -) { - - operator fun invoke(): Flow> { - return notificationsRepository.observeNotifications() - .map { list -> - list - .filter { it.publishTime > timeProvider.systemCurrentTime() - Duration.ofDays(publishedAfterDays).toMillis() } - .sortedWith(compareBy({ it.isRead }, { it.publishTime.unaryMinus() })) - } - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/ReadAllNotificationsCase.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/ReadAllNotificationsCase.kt deleted file mode 100644 index 06b512e5..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/ReadAllNotificationsCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.twofasapp.notifications.domain - -import com.twofasapp.notifications.domain.repository.NotificationsRepository - -internal class ReadAllNotificationsCase( - private val notificationsRepository: NotificationsRepository, -) { - - suspend operator fun invoke() { - return notificationsRepository.readAllNotifications() - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepository.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepository.kt deleted file mode 100644 index f0aaadd1..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.twofasapp.notifications.domain.repository - -import com.twofasapp.notifications.domain.model.Notification -import kotlinx.coroutines.flow.Flow - -internal interface NotificationsRepository { - - companion object { - const val publishedAfterDays = 90L - } - - fun observeNotifications(): Flow> - suspend fun getNotifications(): List - suspend fun fetchNotifications() - suspend fun readAllNotifications() -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepositoryImpl.kt b/notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepositoryImpl.kt deleted file mode 100644 index fa8057cb..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/domain/repository/NotificationsRepositoryImpl.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.twofasapp.notifications.domain.repository - -import com.twofasapp.notifications.data.NotificationsLocalData -import com.twofasapp.notifications.data.NotificationsRemoteData -import com.twofasapp.notifications.domain.model.Notification -import com.twofasapp.notifications.domain.repository.NotificationsRepository.Companion.publishedAfterDays -import com.twofasapp.prefs.model.CacheEntry -import com.twofasapp.prefs.usecase.CacheValidityPreference -import com.twofasapp.time.domain.TimeProvider -import kotlinx.coroutines.flow.Flow - -internal class NotificationsRepositoryImpl( - private val localData: NotificationsLocalData, - private val remoteData: NotificationsRemoteData, - private val cacheValidity: CacheValidityPreference, - private val timeProvider: TimeProvider, -) : NotificationsRepository { - - override fun observeNotifications(): Flow> { - return localData.observeNotifications() - } - - override suspend fun getNotifications(): List { - return localData.getNotifications() - } - - override suspend fun fetchNotifications() { - cacheValidity.runWithCacheValidation(CacheEntry.FetchNotifications) { - - val remoteData = remoteData.fetchNotifications(timeProvider.currentDateTimeUtc().minusDays(publishedAfterDays)) - localData.saveNotifications(remoteData) - } - } - - override suspend fun readAllNotifications() { - localData.readAllNotifications() - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsActivity.kt b/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsActivity.kt deleted file mode 100644 index 6fc356a8..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsActivity.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.twofasapp.notifications.ui - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.twofasapp.base.BaseComponentActivity -import com.twofasapp.design.compose.Toolbar -import com.twofasapp.design.theme.* -import com.twofasapp.extensions.openBrowserApp -import com.twofasapp.notifications.R -import com.twofasapp.notifications.domain.model.Notification -import com.twofasapp.time.domain.formatter.DurationFormatter -import kotlinx.coroutines.launch -import org.koin.androidx.compose.get -import org.koin.androidx.viewmodel.ext.android.viewModel - -class NotificationsActivity : BaseComponentActivity() { - - private val viewModel: NotificationsViewModel by viewModel() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - AppThemeLegacy { - Scaffold( - topBar = { Toolbar(title = "Notifications") { onBackPressed() } } - ) { padding -> - NotificationList(padding) - } - } - } - - lifecycleScope.launch { - lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.uiState.collect { - it.handleEvent { - when (it) { - is NotificationsUiState.Event.OpenBrowser -> openBrowserApp(url = it.url) - } - viewModel.eventHandled(it.id) - } - - } - } - } - } - - @OptIn(ExperimentalFoundationApi::class) - @Composable - private fun NotificationList(padding: PaddingValues) { - val uiState = viewModel.uiState.collectAsState() - - if (uiState.value.items.isNotEmpty()) { - LazyColumn(modifier = Modifier.padding(padding)) { - items(uiState.value.items, key = { it.id }) { - NotificationItem(it, modifier = Modifier.animateItemPlacement()) - } - } - } else { - Text( - text = "No new messages", - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - style = MaterialTheme.typography.subtitle2.copy(color = MaterialTheme.colors.textSecondary, fontSize = 16.sp), - textAlign = TextAlign.Center, - ) - } - } - - @Composable - private fun NotificationItem( - notification: Notification, - modifier: Modifier = Modifier, - durationFormatter: DurationFormatter = get(), - ) { - Column( - modifier = modifier - .animateContentSize() - .background(if (notification.isRead) MaterialTheme.colors.backgroundSecondary else MaterialTheme.colors.background) - .clickable { viewModel.itemClicked(notification) } - .padding(16.dp) - ) { - - Row { - Image( - painter = painterResource( - when (notification.category) { - Notification.Category.Updates -> R.drawable.notif_category_update - Notification.Category.News -> R.drawable.notif_category_news - Notification.Category.Features -> R.drawable.notif_category_feature - Notification.Category.Youtube -> R.drawable.notif_category_video - } - ), - contentDescription = null, - modifier = Modifier - .size(48.dp) - .padding(top = 6.dp) - ) - - Spacer(modifier = Modifier.width(16.dp)) - - Text( - text = notification.message, - modifier = Modifier.weight(1f), - style = TextStyle( - color = MaterialTheme.colors.textPrimary, - fontSize = 18.sp - ), - ) - - Spacer(modifier = Modifier.width(16.dp)) - - IconButton( - modifier = Modifier - .size(28.dp) - .padding(top = 6.dp), onClick = { viewModel.itemClicked(notification) } - ) { - Icon(painterResource(com.twofasapp.resources.R.drawable.ic_external_link), null, tint = MaterialTheme.colors.icon) - } - } - - Spacer(modifier = Modifier.height(4.dp)) - - Row { - Spacer(modifier = Modifier.width(62.dp)) - Text( - text = durationFormatter.format(notification.publishTime), - style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.textSecondary), - ) - } - } - } -} diff --git a/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsUiState.kt b/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsUiState.kt deleted file mode 100644 index 35fc13eb..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsUiState.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.twofasapp.notifications.ui - -import com.twofasapp.base.UiEvent -import com.twofasapp.base.UiState -import com.twofasapp.notifications.domain.model.Notification - -internal data class NotificationsUiState( - val items: List = emptyList(), - override val events: List = emptyList() -) : UiState { - - sealed class Event : UiEvent() { - class OpenBrowser(val url: String): Event() - } - - override fun copyStateWithNewEvents(events: List): NotificationsUiState { - return copy(events = events) - } -} \ No newline at end of file diff --git a/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsViewModel.kt b/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsViewModel.kt deleted file mode 100644 index 853621be..00000000 --- a/notifications/src/main/java/com/twofasapp/notifications/ui/NotificationsViewModel.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.twofasapp.notifications.ui - -import androidx.lifecycle.viewModelScope -import com.twofasapp.base.BaseViewModel -import com.twofasapp.base.dispatcher.Dispatchers -import com.twofasapp.core.analytics.AnalyticsEvent -import com.twofasapp.core.analytics.AnalyticsParam -import com.twofasapp.core.analytics.AnalyticsService -import com.twofasapp.notifications.domain.GetNotificationsCase -import com.twofasapp.notifications.domain.ReadAllNotificationsCase -import com.twofasapp.notifications.domain.model.Notification -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -internal class NotificationsViewModel( - private val dispatchers: Dispatchers, - private val getNotificationsCase: GetNotificationsCase, - private val readAllNotificationsCase: ReadAllNotificationsCase, - private val analyticsService: AnalyticsService, -) : BaseViewModel() { - - private val _uiState = MutableStateFlow(NotificationsUiState()) - val uiState = _uiState.asStateFlow() - - init { - viewModelScope.launch(dispatchers.io()) { - val notifications = getNotificationsCase() - _uiState.update { it.copy(items = notifications) } - - readAllNotificationsCase() - } - } - - fun itemClicked(notification: Notification) { - viewModelScope.launch(dispatchers.io()) { - analyticsService.captureEvent( - AnalyticsEvent.NEWS_CLICK, - AnalyticsParam.ID to notification.id, - ) - _uiState.update { - it.copy(items = it.items.map { item -> - if (item.id == notification.id) { - item.copy(isRead = true) - } else { - item - } - }) - } - - _uiState.update { it.postEvent(NotificationsUiState.Event.OpenBrowser(notification.link)) } - } - } - - fun eventHandled(id: String) { - _uiState.update { it.reduceEvent(id) } - } -} \ No newline at end of file diff --git a/parsers/build.gradle.kts b/parsers/build.gradle.kts index 66cd1c84..4036362e 100644 --- a/parsers/build.gradle.kts +++ b/parsers/build.gradle.kts @@ -8,6 +8,6 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":prefs")) } \ No newline at end of file diff --git a/permissions/build.gradle.kts b/permissions/build.gradle.kts index 65c95783..f38c0066 100644 --- a/permissions/build.gradle.kts +++ b/permissions/build.gradle.kts @@ -8,7 +8,7 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":resources")) implementation(project(":extensions")) diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index 838cd57c..cf72971c 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -15,10 +15,14 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":prefs")) - implementation(project(":environment")) + implementation(project(":parsers")) + implementation(project(":data:notifications")) + implementation(project(":data:services")) + implementation(project(":data:browserext")) + implementation(project(":core:common")) implementation(libs.bundles.room) kapt(libs.roomCompiler) diff --git a/persistence/src/main/java/com/twofasapp/persistence/AppDatabase.kt b/persistence/src/main/java/com/twofasapp/persistence/AppDatabase.kt index 2b1d34cf..130a8f9e 100644 --- a/persistence/src/main/java/com/twofasapp/persistence/AppDatabase.kt +++ b/persistence/src/main/java/com/twofasapp/persistence/AppDatabase.kt @@ -9,15 +9,15 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.twofasapp.data.notifications.local.NotificationsDao +import com.twofasapp.data.notifications.local.model.NotificationEntity +import com.twofasapp.data.services.local.ServiceDao +import com.twofasapp.data.services.local.model.ServiceEntity import com.twofasapp.parsers.LegacyTypeToId import com.twofasapp.parsers.ServiceIcons import com.twofasapp.persistence.converter.Converters -import com.twofasapp.persistence.dao.NotificationDao -import com.twofasapp.persistence.dao.PairedBrowserDao -import com.twofasapp.persistence.dao.ServiceDao -import com.twofasapp.persistence.model.NotificationEntity -import com.twofasapp.persistence.model.PairedBrowserEntity -import com.twofasapp.persistence.model.ServiceEntity +import com.twofasapp.data.browserext.local.PairedBrowserDao +import com.twofasapp.data.browserext.local.model.PairedBrowserEntity @Database( entities = [ @@ -40,7 +40,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun serviceDao(): ServiceDao abstract fun pairedBrowserDao(): PairedBrowserDao - abstract fun notificationDao(): NotificationDao + abstract fun notificationDao(): NotificationsDao } val MIGRATION_1_2 = object : Migration(1, 2) { diff --git a/persistence/src/main/java/com/twofasapp/persistence/PersistenceModule.kt b/persistence/src/main/java/com/twofasapp/persistence/PersistenceModule.kt index 4a3f6e04..23f00e2a 100644 --- a/persistence/src/main/java/com/twofasapp/persistence/PersistenceModule.kt +++ b/persistence/src/main/java/com/twofasapp/persistence/PersistenceModule.kt @@ -1,8 +1,8 @@ package com.twofasapp.persistence import androidx.room.Room +import com.twofasapp.common.environment.AppBuild import com.twofasapp.di.KoinModule -import com.twofasapp.environment.AppConfig import com.twofasapp.persistence.cipher.DatabaseKeyGenerator import com.twofasapp.persistence.cipher.DatabaseKeyGeneratorRandom import com.twofasapp.persistence.cipher.GetDatabaseMasterKey @@ -34,7 +34,7 @@ class PersistenceModule : KoinModule { MIGRATION_9_10, ) - if (get().isDebug.not()) { + if (get().isDebuggable.not()) { val factory = SupportFactory(SQLiteDatabase.getBytes(get().execute().toCharArray())) builder.openHelperFactory(factory) } diff --git a/persistence/src/main/java/com/twofasapp/persistence/dao/ServiceDao.kt b/persistence/src/main/java/com/twofasapp/persistence/dao/ServiceDao.kt deleted file mode 100644 index 545ccfb6..00000000 --- a/persistence/src/main/java/com/twofasapp/persistence/dao/ServiceDao.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.twofasapp.persistence.dao - -import androidx.room.* -import com.twofasapp.persistence.model.ServiceEntity -import io.reactivex.Completable -import io.reactivex.Flowable -import io.reactivex.Single -import kotlinx.coroutines.flow.Flow - -@Dao -interface ServiceDao { - @Query("SELECT * FROM local_services") - fun select(): Single> - - @Query("SELECT * FROM local_services") - suspend fun selectAll(): List - - @Query("SELECT * FROM local_services") - fun selectFlow(): Flow> - - @Query("SELECT * FROM local_services") - fun observe(): Flowable> - - @Query("SELECT * FROM local_services WHERE id=:serviceId") - fun observe(serviceId: Long): Flow - - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(serviceEntity: ServiceEntity): Single - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertSuspend(serviceEntity: ServiceEntity): Long - - @Update(onConflict = OnConflictStrategy.REPLACE) - fun update(vararg serviceEntity: ServiceEntity): Completable - - @Update(onConflict = OnConflictStrategy.REPLACE) - suspend fun updateSuspend(vararg serviceEntity: ServiceEntity) - - @Query("DELETE FROM local_services WHERE id IN (:ids)") - fun deleteById(ids: List): Completable - - @Query("DELETE FROM local_services WHERE id == :id") - suspend fun deleteById(id: Long) -} \ No newline at end of file diff --git a/prefs/build.gradle.kts b/prefs/build.gradle.kts index fa1f3c76..f8cb5a96 100644 --- a/prefs/build.gradle.kts +++ b/prefs/build.gradle.kts @@ -10,9 +10,9 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":serialization")) - implementation(project(":environment")) + implementation(project(":time:domain")) implementation(project(":extensions")) diff --git a/prefs/src/main/java/com/twofasapp/prefs/PreferencesPlainModule.kt b/prefs/src/main/java/com/twofasapp/prefs/PreferencesPlainModule.kt index 1e4cb1e4..cbd57f4f 100644 --- a/prefs/src/main/java/com/twofasapp/prefs/PreferencesPlainModule.kt +++ b/prefs/src/main/java/com/twofasapp/prefs/PreferencesPlainModule.kt @@ -12,7 +12,6 @@ import com.twofasapp.prefs.usecase.LastPushesPreference import com.twofasapp.prefs.usecase.LastScannedQrPreference import com.twofasapp.prefs.usecase.LockMethodPreference import com.twofasapp.prefs.usecase.MigratedToRoomPreference -import com.twofasapp.prefs.usecase.MobileDevicePreference import com.twofasapp.prefs.usecase.PinCodePreference import com.twofasapp.prefs.usecase.RateAppStatusPreference import com.twofasapp.prefs.usecase.RemoteBackupStatusPreference @@ -45,7 +44,6 @@ class PreferencesPlainModule : KoinModule { single { WidgetSettingsPreference(get()) } single { AppUpdateLastCheckVersionPreference(get()) } single { CurrentAppVersionPreference(get()) } - single { MobileDevicePreference(get()) } single { LastPushesPreference(get()) } single { CacheValidityPreference(get(), get()) } } diff --git a/prefs/src/main/java/com/twofasapp/prefs/usecase/MobileDevicePreference.kt b/prefs/src/main/java/com/twofasapp/prefs/usecase/MobileDevicePreference.kt deleted file mode 100644 index 132e3667..00000000 --- a/prefs/src/main/java/com/twofasapp/prefs/usecase/MobileDevicePreference.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.twofasapp.prefs.usecase - -import com.twofasapp.storage.Preferences -import com.twofasapp.prefs.internals.PreferenceModel -import com.twofasapp.prefs.model.MobileDeviceEntity - -class MobileDevicePreference(preferences: Preferences) : PreferenceModel(preferences) { - - override val key: String = "mobileDevice" - override val default: MobileDeviceEntity = MobileDeviceEntity( - id = "", - name = "", - fcmToken = "", - platform = "", - publicKey = "", - ) - - override val serialize: (MobileDeviceEntity) -> String = { jsonSerializer.serialize(it) } - override val deserialize: (String) -> MobileDeviceEntity = { jsonSerializer.deserialize(it) } -} \ No newline at end of file diff --git a/push/build.gradle.kts b/push/build.gradle.kts index 5b84d212..0afa1fd2 100644 --- a/push/build.gradle.kts +++ b/push/build.gradle.kts @@ -8,11 +8,12 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":base")) implementation(project(":prefs")) - implementation(project(":environment")) + implementation(project(":time:domain")) + implementation(project(":core:common")) implementation(libs.kotlinCoroutines) implementation(libs.timber) diff --git a/push/src/main/java/com/twofasapp/push/domain/repository/PushLogger.kt b/push/src/main/java/com/twofasapp/push/domain/repository/PushLogger.kt index 51881608..f360b169 100644 --- a/push/src/main/java/com/twofasapp/push/domain/repository/PushLogger.kt +++ b/push/src/main/java/com/twofasapp/push/domain/repository/PushLogger.kt @@ -1,20 +1,20 @@ package com.twofasapp.push.domain.repository import com.google.firebase.messaging.RemoteMessage -import com.twofasapp.environment.AppConfig +import com.twofasapp.common.environment.AppBuild import com.twofasapp.prefs.model.LastPushesEntity import com.twofasapp.prefs.usecase.LastPushesPreference import com.twofasapp.time.domain.TimeProvider import timber.log.Timber class PushLogger( - private val appConfig: AppConfig, + private val appBuild: AppBuild, private val timeProvider: TimeProvider, private val lastPushesPreference: LastPushesPreference, ) { fun logMessage(remoteMessage: RemoteMessage) { - if (appConfig.isDebug) { + if (appBuild.isDebuggable) { try { Timber.i("Data: ${remoteMessage.data}, notification.title=${remoteMessage.notification?.title}, notification.body=${remoteMessage.notification?.body}") lastPushesPreference.put { @@ -38,7 +38,7 @@ class PushLogger( } fun logToken(token: String) { - if (appConfig.isDebug) { + if (appBuild.isDebuggable) { Timber.i("onNewToken") Timber.i(token) } diff --git a/qrscanner/build.gradle.kts b/qrscanner/build.gradle.kts index 523e78d0..5e49a7c5 100644 --- a/qrscanner/build.gradle.kts +++ b/qrscanner/build.gradle.kts @@ -10,7 +10,7 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":design")) implementation(project(":resources")) implementation(project(":extensions")) diff --git a/qrscanner/src/main/java/com/twofasapp/qrscanner/ui/QrScannerScreen.kt b/qrscanner/src/main/java/com/twofasapp/qrscanner/ui/QrScannerScreen.kt index 78db43cb..0d51e839 100644 --- a/qrscanner/src/main/java/com/twofasapp/qrscanner/ui/QrScannerScreen.kt +++ b/qrscanner/src/main/java/com/twofasapp/qrscanner/ui/QrScannerScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf @@ -58,6 +58,7 @@ fun QrScannerScreen( viewModel: QrScannerViewModel = get(), isGalleryEnabled: Boolean = false, startWithGallery: Boolean = false, + modifier: Modifier = Modifier ) { val showReadError = viewModel.showPhotoReadError.collectAsState().value @@ -68,27 +69,26 @@ fun QrScannerScreen( val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) - - Scaffold( floatingActionButton = { if (isGalleryEnabled) { FloatingActionButton( onClick = { galleryLauncher.launch(galleryIntent) }, - backgroundColor = Color(0xFF4C4C4C), + containerColor = Color(0xFF4C4C4C), ) { Icon(painter = painterResource(id = R.drawable.ic_photo_gallery), "", tint = Color.White) } } - } + }, + modifier = modifier ) { padding -> - Box(modifier = Modifier.padding(padding)) { + Box() { QrScannerPreview(onScanned = { viewModel.onScanned(it) }) Text( text = stringResource(id = R.string.tokens__scan_qr_code_title), - style = MaterialTheme.typography.h5.copy(fontSize = 20.sp), color = Color.White, + style = MaterialTheme.typography.bodySmall.copy(fontSize = 20.sp), color = Color.White, textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() diff --git a/resources/src/main/res/values/colors.xml b/resources/src/main/res/values/colors.xml index 4fe3d5a9..dc68d9d5 100644 --- a/resources/src/main/res/values/colors.xml +++ b/resources/src/main/res/values/colors.xml @@ -1,7 +1,7 @@ #FFF - #F9F9F9 + #FFF #F2F2F2 #000 #FFF diff --git a/security/build.gradle.kts b/security/build.gradle.kts index 4d2fd416..4de9f260 100644 --- a/security/build.gradle.kts +++ b/security/build.gradle.kts @@ -10,7 +10,7 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":resources")) implementation(project(":extensions")) implementation(project(":design")) @@ -20,6 +20,8 @@ dependencies { implementation(project(":security:domain")) implementation(project(":time:domain")) + implementation(project(":core:designsystem")) + implementation(libs.bundles.appCompat) implementation(libs.bundles.compose) implementation(libs.biometric) diff --git a/security/domain/build.gradle.kts b/security/domain/build.gradle.kts index 2e7595f9..baadfa9c 100644 --- a/security/domain/build.gradle.kts +++ b/security/domain/build.gradle.kts @@ -10,15 +10,14 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":design")) implementation(project(":extensions")) implementation(project(":permissions")) implementation(project(":prefs")) implementation(project(":persistence")) - implementation(project(":network")) implementation(project(":push")) - implementation(project(":environment")) + implementation(project(":resources")) implementation(project(":parsers")) implementation(project(":truetime")) diff --git a/security/src/main/java/com/twofasapp/security/ui/changepin/ChangePinScreen.kt b/security/src/main/java/com/twofasapp/security/ui/changepin/ChangePinScreen.kt index e8f03fe3..59edb5f0 100644 --- a/security/src/main/java/com/twofasapp/security/ui/changepin/ChangePinScreen.kt +++ b/security/src/main/java/com/twofasapp/security/ui/changepin/ChangePinScreen.kt @@ -3,14 +3,14 @@ package com.twofasapp.security.ui.changepin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.Scaffold +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import com.twofasapp.design.compose.Toolbar +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.navigation.SecurityDirections import com.twofasapp.navigation.SecurityRouter import com.twofasapp.resources.R @@ -39,7 +39,7 @@ internal fun ChangePinScreen( Scaffold( topBar = { - Toolbar(title = stringResource(id = R.string.security__change_pin)) { router.navigateBack() } + TwTopAppBar(titleText = stringResource(id = R.string.security__change_pin)) } ) { padding -> Box( diff --git a/security/src/main/java/com/twofasapp/security/ui/disablepin/DisablePinScreen.kt b/security/src/main/java/com/twofasapp/security/ui/disablepin/DisablePinScreen.kt index fb4b57cc..158b5c85 100644 --- a/security/src/main/java/com/twofasapp/security/ui/disablepin/DisablePinScreen.kt +++ b/security/src/main/java/com/twofasapp/security/ui/disablepin/DisablePinScreen.kt @@ -3,14 +3,14 @@ package com.twofasapp.security.ui.disablepin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.Scaffold +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import com.twofasapp.design.compose.Toolbar +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.navigation.SecurityRouter import com.twofasapp.resources.R import com.twofasapp.security.ui.pin.PinScreen @@ -38,7 +38,7 @@ internal fun DisablePinScreen( Scaffold( topBar = { - Toolbar(title = stringResource(id = R.string.security__disable_pin)) { router.navigateBack() } + TwTopAppBar(titleText = stringResource(id = R.string.security__disable_pin)) } ) { padding -> Box( diff --git a/security/src/main/java/com/twofasapp/security/ui/pin/PinInput.kt b/security/src/main/java/com/twofasapp/security/ui/pin/PinInput.kt index 11d71299..35ad856a 100644 --- a/security/src/main/java/com/twofasapp/security/ui/pin/PinInput.kt +++ b/security/src/main/java/com/twofasapp/security/ui/pin/PinInput.kt @@ -4,9 +4,18 @@ import android.content.Context import android.os.Vibrator import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,7 +23,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.twofasapp.design.theme.divider +import com.twofasapp.designsystem.TwTheme import com.twofasapp.resources.R @Composable @@ -45,11 +54,11 @@ internal fun PinInput( .size(12.dp) .run { if (index < enteredDigits) { - background(shape = CircleShape, color = MaterialTheme.colors.primary) + background(shape = CircleShape, color = TwTheme.color.primary) } else { - background(shape = CircleShape, color = MaterialTheme.colors.background) - border(width = 2.dp, color = MaterialTheme.colors.divider, shape = CircleShape) + background(shape = CircleShape, color = TwTheme.color.background) + border(width = 2.dp, color = TwTheme.color.divider, shape = CircleShape) } } @@ -82,7 +91,7 @@ internal fun PinInput( } } - Divider(color = MaterialTheme.colors.divider) + Divider(color = TwTheme.color.divider) } } diff --git a/security/src/main/java/com/twofasapp/security/ui/pin/PinKeyboard.kt b/security/src/main/java/com/twofasapp/security/ui/pin/PinKeyboard.kt index e68b6d2b..fd6e012f 100644 --- a/security/src/main/java/com/twofasapp/security/ui/pin/PinKeyboard.kt +++ b/security/src/main/java/com/twofasapp/security/ui/pin/PinKeyboard.kt @@ -8,9 +8,8 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,7 +20,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.twofasapp.design.theme.textPrimary +import com.twofasapp.designsystem.TwTheme import com.twofasapp.resources.R internal enum class Keys { @@ -92,7 +91,7 @@ internal fun PinKeyboard( Icon( painterResource(id = R.drawable.key_fingerprint), null, - tint = MaterialTheme.colors.textPrimary, + tint = TwTheme.color.onSurfacePrimary, modifier = Modifier .align(Alignment.Center) .alpha(alpha), @@ -105,7 +104,7 @@ internal fun PinKeyboard( modifier = Modifier .align(Alignment.Center) .alpha(alpha), - color = MaterialTheme.colors.textPrimary, + color = TwTheme.color.onSurfacePrimary, fontWeight = FontWeight.Light ) } diff --git a/security/src/main/java/com/twofasapp/security/ui/pin/PinScreen.kt b/security/src/main/java/com/twofasapp/security/ui/pin/PinScreen.kt index 4821f48f..64d62858 100644 --- a/security/src/main/java/com/twofasapp/security/ui/pin/PinScreen.kt +++ b/security/src/main/java/com/twofasapp/security/ui/pin/PinScreen.kt @@ -1,11 +1,25 @@ package com.twofasapp.security.ui.pin -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -15,7 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import com.twofasapp.design.compose.ProgressBar -import com.twofasapp.design.theme.textPrimary +import com.twofasapp.designsystem.TwTheme import com.twofasapp.resources.R import com.twofasapp.security.ui.biometric.BiometricDialog @@ -75,7 +89,7 @@ internal fun PinScreen( .fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.body1, - color = if (errorMessage.isNotBlank()) MaterialTheme.colors.primary else MaterialTheme.colors.textPrimary + color = if (errorMessage.isNotBlank()) TwTheme.color.primary else TwTheme.color.onSurfacePrimary ) PinInput( diff --git a/security/src/main/java/com/twofasapp/security/ui/security/SecurityActivity.kt b/security/src/main/java/com/twofasapp/security/ui/security/SecurityActivity.kt index 8e90e534..e17deee8 100644 --- a/security/src/main/java/com/twofasapp/security/ui/security/SecurityActivity.kt +++ b/security/src/main/java/com/twofasapp/security/ui/security/SecurityActivity.kt @@ -7,8 +7,10 @@ import androidx.compose.runtime.compositionLocalOf import androidx.lifecycle.ViewModelStoreOwner import com.twofasapp.base.BaseComponentActivity import com.twofasapp.design.theme.AppThemeLegacy +import com.twofasapp.designsystem.MainAppTheme import com.twofasapp.navigation.SecurityRouter import com.twofasapp.navigation.base.RouterNavHost +import com.twofasapp.prefs.model.AppTheme import org.koin.androidx.compose.get import org.koin.androidx.viewmodel.ext.android.viewModel @@ -24,7 +26,7 @@ class SecurityActivity : BaseComponentActivity() { viewModel.init() setContent { - AppThemeLegacy { + MainAppTheme { Surface { RouterNavHost(router = get(), viewModelStoreOwner.current) } diff --git a/security/src/main/java/com/twofasapp/security/ui/security/SecurityScreen.kt b/security/src/main/java/com/twofasapp/security/ui/security/SecurityScreen.kt index e8a54e52..23bb5e84 100644 --- a/security/src/main/java/com/twofasapp/security/ui/security/SecurityScreen.kt +++ b/security/src/main/java/com/twofasapp/security/ui/security/SecurityScreen.kt @@ -3,11 +3,16 @@ package com.twofasapp.security.ui.security import android.app.Activity import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Divider import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material3.Divider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -15,11 +20,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentActivity -import com.twofasapp.design.compose.* +import com.twofasapp.design.compose.HeaderEntry +import com.twofasapp.design.compose.SimpleEntry +import com.twofasapp.design.compose.SubtitleGravity +import com.twofasapp.design.compose.SwitchEntry import com.twofasapp.design.compose.dialogs.ListDialog -import com.twofasapp.design.theme.divider -import com.twofasapp.design.theme.textPrimary -import com.twofasapp.design.theme.textSecondary +import com.twofasapp.designsystem.TwTheme +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.navigation.SecurityDirections import com.twofasapp.navigation.SecurityRouter import com.twofasapp.resources.R @@ -44,7 +51,7 @@ internal fun SecurityScreen( Scaffold( topBar = { - Toolbar(title = stringResource(id = R.string.settings__security)) { activity?.onBackPressed() } + TwTopAppBar(titleText = stringResource(id = R.string.settings__security)) } ) { padding -> LazyColumn(modifier = Modifier.padding(padding)) { @@ -58,7 +65,7 @@ internal fun SecurityScreen( ) } - item { Divider(color = MaterialTheme.colors.divider) } + item { Divider(color = TwTheme.color.divider) } item { HeaderEntry(text = stringResource(id = R.string.settings__biometrics)) } item { @@ -75,7 +82,7 @@ internal fun SecurityScreen( Text( text = stringResource(id = R.string.settings__option_fingerprint_description), modifier = Modifier.padding(start = 24.dp, end = 16.dp, top = 8.dp), - style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.textSecondary) + style = MaterialTheme.typography.body2.copy(color = TwTheme.color.onSurfaceSecondary) ) } @@ -100,7 +107,7 @@ internal fun SecurityScreen( ) } - item { Divider(color = MaterialTheme.colors.divider) } + item { Divider(color = TwTheme.color.divider) } item { HeaderEntry(text = stringResource(id = R.string.settings__app_blocking)) } item { @@ -116,8 +123,8 @@ internal fun SecurityScreen( item { Text( text = stringResource(id = R.string.settings__how_many_attempts_footer), - modifier = Modifier.padding(start = 72.dp, end = 16.dp,), - style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = MaterialTheme.colors.textSecondary), + modifier = Modifier.padding(start = 72.dp, end = 16.dp), + style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = TwTheme.color.onSurfaceSecondary), ) } @@ -135,12 +142,12 @@ internal fun SecurityScreen( item { Text( text = stringResource(id = R.string.settings__block_for_footer), - modifier = Modifier.padding(start = 72.dp, end = 16.dp,), - style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = MaterialTheme.colors.textSecondary), + modifier = Modifier.padding(start = 72.dp, end = 16.dp), + style = MaterialTheme.typography.body2.copy(fontSize = 14.sp, color = TwTheme.color.onSurfaceSecondary), ) } - item { Divider(color = MaterialTheme.colors.divider) } + item { Divider(color = TwTheme.color.divider) } item { HeaderEntry(text = stringResource(id = R.string.settings__biometrics)) } item { @@ -158,7 +165,7 @@ internal fun SecurityScreen( ) } - item { Divider(color = MaterialTheme.colors.divider) } + item { Divider(color = TwTheme.color.divider) } } } diff --git a/security/src/main/java/com/twofasapp/security/ui/setuppin/SetupPinScreen.kt b/security/src/main/java/com/twofasapp/security/ui/setuppin/SetupPinScreen.kt index 3040007a..82900921 100644 --- a/security/src/main/java/com/twofasapp/security/ui/setuppin/SetupPinScreen.kt +++ b/security/src/main/java/com/twofasapp/security/ui/setuppin/SetupPinScreen.kt @@ -3,17 +3,22 @@ package com.twofasapp.security.ui.setuppin import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.* +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.twofasapp.design.compose.Toolbar import com.twofasapp.design.compose.dialogs.ListDialog +import com.twofasapp.designsystem.common.TwTopAppBar import com.twofasapp.navigation.SecurityRouter import com.twofasapp.resources.R import com.twofasapp.security.domain.model.PinDigits @@ -47,7 +52,7 @@ internal fun SetupPinScreen( Scaffold( topBar = { - Toolbar(title = stringResource(id = R.string.security__create_pin)) { router.navigateBack() } + TwTopAppBar(titleText = stringResource(id = R.string.security__create_pin)) } ) { padding -> Box( diff --git a/serialization/build.gradle.kts b/serialization/build.gradle.kts index de66b601..63fa4212 100644 --- a/serialization/build.gradle.kts +++ b/serialization/build.gradle.kts @@ -9,6 +9,6 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") } \ No newline at end of file diff --git a/services/build.gradle.kts b/services/build.gradle.kts index 96670474..7c0d632a 100644 --- a/services/build.gradle.kts +++ b/services/build.gradle.kts @@ -10,7 +10,7 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":core")) implementation(project(":design")) implementation(project(":extensions")) @@ -18,10 +18,9 @@ dependencies { implementation(project(":prefs")) implementation(project(":resources")) implementation(project(":persistence")) - implementation(project(":network")) implementation(project(":push")) implementation(project(":qrscanner")) - implementation(project(":environment")) + implementation(project(":services:domain")) implementation(project(":widgets:domain")) implementation(project(":time:domain")) @@ -29,6 +28,7 @@ dependencies { implementation(project(":parsers")) implementation(project(":backup:domain")) implementation(project(":spanner")) + implementation(project(":data:services")) implementation(libs.bundles.fastAdapter) implementation(libs.bundles.rxJava) diff --git a/services/domain/build.gradle.kts b/services/domain/build.gradle.kts index 396d771b..b11ffeb0 100644 --- a/services/domain/build.gradle.kts +++ b/services/domain/build.gradle.kts @@ -10,15 +10,14 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":design")) implementation(project(":extensions")) implementation(project(":permissions")) implementation(project(":prefs")) implementation(project(":persistence")) - implementation(project(":network")) implementation(project(":push")) - implementation(project(":environment")) + implementation(project(":resources")) implementation(project(":parsers")) implementation(project(":time:domain")) @@ -30,4 +29,5 @@ dependencies { implementation(libs.bundles.rxJava) implementation(libs.bundles.appCompat) implementation(libs.bundles.compose) + implementation(libs.apacheCommonsCodec) } diff --git a/services/domain/src/main/java/com/twofasapp/services/domain/otp/InvalidSecretFormat.kt b/services/domain/src/main/java/com/twofasapp/services/domain/otp/InvalidSecretFormat.kt new file mode 100644 index 00000000..dd2db744 --- /dev/null +++ b/services/domain/src/main/java/com/twofasapp/services/domain/otp/InvalidSecretFormat.kt @@ -0,0 +1,3 @@ +package com.twofasapp.services.domain.otp + +class InvalidSecretFormat(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/services/src/main/java/com/twofasapp/services/data/ServicesLocalDataImpl.kt b/services/src/main/java/com/twofasapp/services/data/ServicesLocalDataImpl.kt index c373e16e..3784f8ce 100644 --- a/services/src/main/java/com/twofasapp/services/data/ServicesLocalDataImpl.kt +++ b/services/src/main/java/com/twofasapp/services/data/ServicesLocalDataImpl.kt @@ -2,8 +2,7 @@ package com.twofasapp.services.data import com.twofasapp.extensions.removeWhiteCharacters import com.twofasapp.parsers.ServiceIcons -import com.twofasapp.persistence.dao.ServiceDao -import com.twofasapp.persistence.model.ServiceEntity +import com.twofasapp.data.services.local.ServiceDao import com.twofasapp.prefs.model.BackupSyncStatus import com.twofasapp.prefs.model.ServiceDto import com.twofasapp.prefs.model.Tint @@ -22,7 +21,7 @@ internal class ServicesLocalDataImpl( ) : ServicesLocalData { override fun select(): Single> { - return dao.select() + return dao.legacySelect() .map { list -> list.map { local -> ServiceDto( @@ -61,21 +60,21 @@ internal class ServicesLocalDataImpl( } override suspend fun selectAll(): List { - return dao.selectAll().map { it.toService() } + return dao.legacySelectAll().map { it.toService() } } override fun selectFlow(): Flow> { - return dao.selectFlow().map { list -> + return dao.legacySelectFlow().map { list -> list.map { it.toService() } } } override fun observe(serviceId: Long): Flow { - return dao.observe(serviceId).map { it.toService() } + return dao.legacyObserve(serviceId).map { it.toService() } } override fun observe(): Flowable> { - return dao.observe() + return dao.legacyObserve() .map { list -> list.map { local -> ServiceDto( @@ -115,8 +114,8 @@ internal class ServicesLocalDataImpl( override fun insertService(service: ServiceDto): Single { Timber.d("InsertService: $service") - return dao.insert( - ServiceEntity( + return dao.legacyInsert( + com.twofasapp.data.services.local.model.ServiceEntity( id = 0, name = service.name, secret = service.secret.removeWhiteCharacters(), @@ -146,19 +145,19 @@ internal class ServicesLocalDataImpl( } override suspend fun delete(id: Long) { - dao.deleteById(id) + dao.legacyDeleteById(id) } override suspend fun insertService(service: Service): Long { Timber.d("InsertService: $service") - return dao.insertSuspend(service.toEntity()) + return dao.legacyInsertSuspend(service.toEntity()) } override fun updateService(vararg services: ServiceDto): Completable { Timber.d("UpdateServices: ${services.toList()}") - return dao.update( + return dao.legacyUpdate( *services.map { - ServiceEntity( + com.twofasapp.data.services.local.model.ServiceEntity( id = it.id, name = it.name, secret = it.secret, @@ -189,16 +188,16 @@ internal class ServicesLocalDataImpl( } override suspend fun updateServiceSuspend(vararg services: Service) { - dao.updateSuspend(*services.map { it.toEntity() }.toTypedArray()) + dao.legacyUpdateSuspend(*services.map { it.toEntity() }.toTypedArray()) } override fun deleteService(vararg services: ServiceDto): Completable { Timber.d("DeleteServices: ${services.toList()}") - return dao.deleteById(services.map { it.id }) + return dao.legacyDeleteById(services.map { it.id }) } override fun deleteService(id: Long): Completable { Timber.d("DeleteService: $id") - return dao.deleteById(listOf(id)) + return dao.legacyDeleteById(listOf(id)) } } \ No newline at end of file diff --git a/services/src/main/java/com/twofasapp/services/data/converter/ServiceConverter.kt b/services/src/main/java/com/twofasapp/services/data/converter/ServiceConverter.kt index 39bf19c5..061b1305 100644 --- a/services/src/main/java/com/twofasapp/services/data/converter/ServiceConverter.kt +++ b/services/src/main/java/com/twofasapp/services/data/converter/ServiceConverter.kt @@ -2,13 +2,13 @@ package com.twofasapp.services.data.converter import com.twofasapp.extensions.removeWhiteCharacters import com.twofasapp.parsers.ServiceIcons -import com.twofasapp.persistence.model.ServiceEntity +import com.twofasapp.data.services.local.model.ServiceEntity import com.twofasapp.prefs.model.BackupSyncStatus import com.twofasapp.prefs.model.ServiceDto import com.twofasapp.prefs.model.Tint import com.twofasapp.services.domain.model.Service -internal fun ServiceEntity.toService() = Service( +internal fun com.twofasapp.data.services.local.model.ServiceEntity.toService() = Service( id = id, name = name, secret = secret, @@ -43,7 +43,7 @@ internal fun ServiceEntity.toService() = Service( source = source?.let { Service.Source.valueOf(it) } ?: Service.DefaultSource, ) -internal fun Service.toEntity() = ServiceEntity( +internal fun Service.toEntity() = com.twofasapp.data.services.local.model.ServiceEntity( id = id, name = name, secret = secret.removeWhiteCharacters(), diff --git a/settings.gradle.kts b/settings.gradle.kts index b5ef6e32..1cd873f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,25 +26,20 @@ include(":browserextension") include(":base") include(":prefs") include(":serialization") -include(":di") include(":time") include(":parsers") include(":resources") include(":extensions") include(":design") include(":permissions") -include(":environment") include(":network") include(":push") include(":persistence") include(":qrscanner") -include(":about") -include(":settings") include(":widgets") include(":services") include(":services:domain") include(":widgets:domain") -include(":notifications") include(":navigation") include(":backup") include(":core") @@ -56,12 +51,21 @@ include(":security:domain") include(":start") include(":start:domain") include(":time:domain") -include(":externalimport") -include(":browserextension:domain") +include(":feature:externalimport") include(":data:session") include(":core:storage") include(":core:common") +include(":core:di") include(":core:designsystem") include(":feature:startup") include(":core:locale") include(":feature:home") +include(":data:notifications") +include(":core:network") +include(":data:services") +include(":feature:trash") +include(":feature:about") +include(":data:browserext") +include(":feature:browserext") +include(":core:otp") +include(":feature:appsettings") diff --git a/settings/build.gradle.kts b/settings/build.gradle.kts deleted file mode 100644 index 4e0a4732..00000000 --- a/settings/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -@Suppress("DSL_SCOPE_VIOLATION") -plugins { - alias(libs.plugins.twofasAndroidLibrary) - alias(libs.plugins.twofasCompose) -} - -android { - namespace = "com.twofasapp.settings" -} - -dependencies { - implementation(project(":base")) - implementation(project(":di")) - implementation(project(":resources")) - implementation(project(":extensions")) - implementation(project(":design")) - implementation(project(":prefs")) - implementation(project(":navigation")) - implementation(project(":featuretoggle")) - - implementation(libs.bundles.appCompat) - implementation(libs.bundles.compose) -} \ No newline at end of file diff --git a/settings/src/main/AndroidManifest.xml b/settings/src/main/AndroidManifest.xml deleted file mode 100644 index 04ee14a5..00000000 --- a/settings/src/main/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/settings/src/main/java/com/twofasapp/settings/SettingsModule.kt b/settings/src/main/java/com/twofasapp/settings/SettingsModule.kt deleted file mode 100644 index e2866001..00000000 --- a/settings/src/main/java/com/twofasapp/settings/SettingsModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.twofasapp.settings - -import com.twofasapp.di.KoinModule -import com.twofasapp.settings.ui.main.SettingsMainScreenFactory -import com.twofasapp.settings.ui.main.SettingsMainViewModel -import com.twofasapp.settings.ui.theme.ThemeScreenFactory -import com.twofasapp.settings.ui.theme.ThemeViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -class SettingsModule : KoinModule { - - override fun provide() = module { - viewModelOf(::SettingsMainViewModel) - viewModelOf(::ThemeViewModel) - - singleOf(::SettingsMainScreenFactory) - singleOf(::ThemeScreenFactory) - } -} \ No newline at end of file diff --git a/settings/src/main/java/com/twofasapp/settings/ui/SettingsActivity.kt b/settings/src/main/java/com/twofasapp/settings/ui/SettingsActivity.kt deleted file mode 100644 index ab8076fd..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/SettingsActivity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.twofasapp.settings.ui - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.compose.material.Surface -import com.twofasapp.base.BaseComponentActivity -import com.twofasapp.design.theme.AppThemeLegacy -import com.twofasapp.navigation.SettingsRouter -import com.twofasapp.navigation.base.RouterNavHost -import org.koin.androidx.compose.get - -class SettingsActivity : BaseComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - AppThemeLegacy { - Surface { - RouterNavHost(router = get()) - } - } - } - } -} diff --git a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreen.kt b/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreen.kt deleted file mode 100644 index c371d239..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreen.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.twofasapp.settings.ui.main - -import android.app.Activity -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.twofasapp.design.compose.SimpleEntry -import com.twofasapp.design.compose.SubtitleGravity -import com.twofasapp.design.compose.SwitchEntry -import com.twofasapp.design.compose.Toolbar -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter -import com.twofasapp.prefs.model.AppTheme -import com.twofasapp.resources.R -import org.koin.androidx.compose.get - -@Composable -internal fun SettingsMainScreen( - viewModel: SettingsMainViewModel = get(), - router: SettingsRouter = get(), -) { - val uiState = viewModel.uiState.collectAsState().value - val activity = (LocalContext.current as? Activity) - - Scaffold( - topBar = { Toolbar(title = stringResource(id = R.string.settings__settings)) { activity?.onBackPressed() } } - ) { padding -> - LazyColumn { - item { - SimpleEntry( - title = stringResource(id = R.string.settings__option_theme), - subtitle = when (uiState.theme) { - AppTheme.AUTO -> stringResource(R.string.settings__theme_option_auto) - AppTheme.LIGHT -> stringResource(R.string.settings__theme_option_light) - AppTheme.DARK -> stringResource(R.string.settings__theme_option_dark) - }, - subtitleGravity = SubtitleGravity.END, - icon = painterResource(id = R.drawable.ic_option_theme), - click = { router.navigate(SettingsDirections.Theme) } - ) - } - - item { - SwitchEntry( - title = stringResource(id = R.string.settings__show_next_token), - icon = painterResource(id = R.drawable.ic_next_token), - isChecked = uiState.showNextToken, - switch = { isChecked -> viewModel.changeShowNextToken(isChecked) } - ) - } - - item { - SimpleEntry( - title = stringResource(id = R.string.browser__browser_extension), - icon = painterResource(id = R.drawable.ic_option_browser_extension), - click = { router.navigate(SettingsDirections.BrowserExtension) } - ) - } - - } - } -} diff --git a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreenFactory.kt b/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreenFactory.kt deleted file mode 100644 index 505f4099..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.settings.ui.main - -import androidx.compose.runtime.Composable - -class SettingsMainScreenFactory { - - @Composable - fun create() { - SettingsMainScreen() - } -} \ No newline at end of file diff --git a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainUiState.kt b/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainUiState.kt deleted file mode 100644 index bc9f657d..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainUiState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.twofasapp.settings.ui.main - -import com.twofasapp.prefs.model.AppTheme - -internal data class SettingsMainUiState( - val theme: AppTheme = AppTheme.AUTO, - val showNextToken: Boolean = false, -) \ No newline at end of file diff --git a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainViewModel.kt b/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainViewModel.kt deleted file mode 100644 index f5019efc..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/main/SettingsMainViewModel.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.twofasapp.settings.ui.main - -import androidx.lifecycle.viewModelScope -import com.twofasapp.base.BaseViewModel -import com.twofasapp.base.dispatcher.Dispatchers -import com.twofasapp.prefs.usecase.AppThemePreference -import com.twofasapp.prefs.usecase.ShowNextTokenPreference -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -internal class SettingsMainViewModel( - private val dispatchers: Dispatchers, - private val appThemePreference: AppThemePreference, - private val showNextTokenPreference: ShowNextTokenPreference, -) : BaseViewModel() { - - private val _uiState = MutableStateFlow(SettingsMainUiState()) - val uiState = _uiState.asStateFlow() - - init { - viewModelScope.launch { - - launch(dispatchers.io()) { - appThemePreference.flow().collect { - _uiState.update { state -> state.copy(theme = it) } - } - } - - launch(dispatchers.io()) { - showNextTokenPreference.flow().collect { - _uiState.update { state -> state.copy(showNextToken = it) } - } - } - } - } - - fun changeShowNextToken(isChecked: Boolean) { - viewModelScope.launch(dispatchers.io()) { - showNextTokenPreference.put(isChecked) - } - } -} diff --git a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreen.kt b/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreen.kt deleted file mode 100644 index 7ea9e1ad..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreen.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.twofasapp.settings.ui.theme - -import android.app.Activity -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.twofasapp.design.compose.SwitchEntry -import com.twofasapp.design.compose.SwitchEntryType -import com.twofasapp.design.compose.Toolbar -import com.twofasapp.navigation.SettingsDirections -import com.twofasapp.navigation.SettingsRouter -import com.twofasapp.prefs.model.AppTheme -import com.twofasapp.resources.R -import org.koin.androidx.compose.get - -@Composable -internal fun ThemeScreen( - viewModel: ThemeViewModel = get(), - router: SettingsRouter = get() -) { - val uiState = viewModel.uiState.collectAsState().value - - if (uiState.recreateActivity) { - (LocalContext.current as? Activity)?.recreate() - } - - Scaffold( - topBar = { - Toolbar(title = stringResource(id = R.string.settings__option_theme)) { - router.navigate(SettingsDirections.GoBack) - } - } - ) { padding -> - LazyColumn(modifier = Modifier.padding(padding)) { - - item { - SwitchEntry( - title = stringResource(id = R.string.settings__theme_option_auto_system), - isChecked = uiState.theme == AppTheme.AUTO, - type = SwitchEntryType.Radio, - iconVisible = false, - switch = { viewModel.changeTheme(AppTheme.AUTO) } - ) - } - - item { - SwitchEntry( - title = stringResource(id = R.string.settings__theme_option_light), - isChecked = uiState.theme == AppTheme.LIGHT, - type = SwitchEntryType.Radio, - iconVisible = false, - switch = { viewModel.changeTheme(AppTheme.LIGHT) } - ) - } - - item { - SwitchEntry( - title = stringResource(id = R.string.settings__theme_option_dark), - isChecked = uiState.theme == AppTheme.DARK, - type = SwitchEntryType.Radio, - iconVisible = false, - switch = { viewModel.changeTheme(AppTheme.DARK) } - ) - } - } - } -} \ No newline at end of file diff --git a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreenFactory.kt b/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreenFactory.kt deleted file mode 100644 index 10b77dcd..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeScreenFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.twofasapp.settings.ui.theme - -import androidx.compose.runtime.Composable - -class ThemeScreenFactory { - - @Composable - fun create() { - ThemeScreen() - } -} \ No newline at end of file diff --git a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeUiState.kt b/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeUiState.kt deleted file mode 100644 index bf0997a4..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeUiState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.twofasapp.settings.ui.theme - -import com.twofasapp.prefs.model.AppTheme - -internal data class ThemeUiState( - val theme: AppTheme = AppTheme.AUTO, - val recreateActivity: Boolean = false, -) \ No newline at end of file diff --git a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeViewModel.kt b/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeViewModel.kt deleted file mode 100644 index 0731f4a1..00000000 --- a/settings/src/main/java/com/twofasapp/settings/ui/theme/ThemeViewModel.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.twofasapp.settings.ui.theme - -import androidx.lifecycle.viewModelScope -import com.twofasapp.base.BaseViewModel -import com.twofasapp.base.dispatcher.Dispatchers -import com.twofasapp.design.theme.ThemeState -import com.twofasapp.prefs.model.AppTheme -import com.twofasapp.prefs.usecase.AppThemePreference -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -internal class ThemeViewModel( - private val dispatchers: Dispatchers, - private val appThemePreference: AppThemePreference, -) : BaseViewModel() { - - private val _uiState = MutableStateFlow(ThemeUiState()) - val uiState = _uiState.asStateFlow() - - init { - viewModelScope.launch(dispatchers.io()) { - appThemePreference.flow().collect { - _uiState.update { state -> state.copy(theme = it) } - } - } - } - - fun changeTheme(theme: AppTheme) { - if (theme == uiState.value.theme) { - return - } - - appThemePreference.put(theme) - - ThemeState.applyTheme(theme) - - _uiState.update { it.copy(recreateActivity = true) } - } -} \ No newline at end of file diff --git a/start/build.gradle.kts b/start/build.gradle.kts index 1462c6d9..d28255a2 100644 --- a/start/build.gradle.kts +++ b/start/build.gradle.kts @@ -11,11 +11,11 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":core")) implementation(project(":resources")) implementation(project(":extensions")) - implementation(project(":environment")) + implementation(project(":design")) implementation(project(":prefs")) implementation(project(":navigation")) @@ -25,6 +25,7 @@ dependencies { implementation(project(":start:domain")) implementation(project(":time:domain")) implementation(project(":parsers")) + implementation(project(":core:common")) implementation(project(":core:storage")) implementation(libs.bundles.appCompat) diff --git a/start/src/main/java/com/twofasapp/start/StartModule.kt b/start/src/main/java/com/twofasapp/start/StartModule.kt index e8897457..f3397176 100644 --- a/start/src/main/java/com/twofasapp/start/StartModule.kt +++ b/start/src/main/java/com/twofasapp/start/StartModule.kt @@ -13,11 +13,9 @@ import com.twofasapp.start.domain.MigratePinCaseImpl import com.twofasapp.start.domain.MigrateUnknownServicesCase import com.twofasapp.start.domain.MigrateUnknownServicesCaseImpl import com.twofasapp.start.domain.work.OnAppUpdatedWorkDispatcher -import com.twofasapp.start.ui.onboarding.OnboardingViewModel import com.twofasapp.start.work.OnAppUpdatedWorkDispatcherImpl import com.twofasapp.storage.EncryptedPreferences import com.twofasapp.storage.PlainPreferences -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.singleOf import org.koin.dsl.module @@ -25,8 +23,6 @@ import org.koin.dsl.module class StartModule : KoinModule { override fun provide() = module { - viewModelOf(::OnboardingViewModel) - singleOf(::DeeplinkHandler) singleOf(::OnAppUpdatedWorkDispatcherImpl) { bind() } diff --git a/start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingActivity.kt b/start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingActivity.kt deleted file mode 100644 index 7442326b..00000000 --- a/start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingActivity.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.twofasapp.start.ui.onboarding - -import android.os.Bundle -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 -import com.twofasapp.base.BaseActivity -import com.twofasapp.extensions.childViews -import com.twofasapp.extensions.openBrowserApp -import com.twofasapp.navigation.StartDirections -import com.twofasapp.navigation.StartRouter -import com.twofasapp.resources.R -import com.twofasapp.start.databinding.ActivityOnboardingBinding -import com.twofasapp.start.ui.onboarding.step.OnboardingStepFragment -import org.koin.androidx.viewmodel.ext.android.viewModel - -class OnboardingActivity : BaseActivity() { - - private val viewModel: OnboardingViewModel by viewModel() - private val router: StartRouter by injectThis() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(ActivityOnboardingBinding::inflate) - - viewBinding.viewPager.adapter = ScreenSlidePagerAdapter(this) - viewBinding.viewPager.offscreenPageLimit = 1 - viewBinding.skip.setOnClickListener { - viewModel.onSkipClicked() - router.navigate(StartDirections.Main) - finish() - } - - viewBinding.terms.setOnClickListener { - viewModel.onTermsClicked() - openBrowserApp(url = "https://2fas.com/terms-of-service") - } - - viewBinding.next.setOnClickListener { - viewModel.onNextClicked(viewBinding.viewPager.currentItem) - - if (viewBinding.viewPager.currentItem == 3) { - viewModel.onStartUsingClicked() - router.navigate(StartDirections.Main) - finish() - } else { - viewBinding.viewPager.currentItem = viewBinding.viewPager.currentItem + 1 - } - } - - viewBinding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - viewBinding.terms.isVisible = position == 0 - viewBinding.skip.isVisible = position != 0 - viewBinding.next.text = when (position) { - 0 -> getString(R.string.commons__continue) - 3 -> getString(R.string.introduction__title) - else -> getString(R.string.commons__next) - } - viewBinding.dots.isVisible = position != 0 - viewBinding.dots.childViews.forEach { it.isSelected = false } - - when (position) { - 1 -> viewBinding.dot1.isSelected = true - 2 -> viewBinding.dot2.isSelected = true - 3 -> viewBinding.dot3.isSelected = true - } - } - }) - } - - private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { - - override fun getItemCount(): Int = 4 - - override fun createFragment(position: Int): Fragment { - val params = when (position) { - 0 -> OnboardingStepFragment.Params( - title = getString(R.string.introduction__page_1_title), - description = getString(R.string.introduction__page_1_content), - smallImageRes = R.drawable.onboarding_step_one, - ) - - 1 -> OnboardingStepFragment.Params( - title = getString(R.string.introduction__page_2_title), - description = getString(R.string.introduction__page_2_content), - imageRes = R.drawable.onboarding_step_two, - ) - - 2 -> OnboardingStepFragment.Params( - title = getString(R.string.introduction__page_3_title), - description = getString(R.string.introduction__page_3_content), - imageRes = R.drawable.onboarding_step_three, - ) - - 3 -> OnboardingStepFragment.Params( - title = getString(R.string.introduction__page_4_title), - description = getString(R.string.introduction__page_4_content_android), - imageRes = R.drawable.onboarding_step_four, - ) - - else -> OnboardingStepFragment.Params( - title = "", - description = "", - imageRes = R.drawable.logo_2fas, - ) - } - - return OnboardingStepFragment.newInstance(params) - } - } -} \ No newline at end of file diff --git a/start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingViewModel.kt b/start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingViewModel.kt deleted file mode 100644 index 52e1dffa..00000000 --- a/start/src/main/java/com/twofasapp/start/ui/onboarding/OnboardingViewModel.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.twofasapp.start.ui.onboarding - -import com.twofasapp.base.BaseViewModel -import com.twofasapp.core.analytics.AnalyticsEvent -import com.twofasapp.core.analytics.AnalyticsService -import com.twofasapp.start.domain.EditShowOnboardingCase - -internal class OnboardingViewModel( - private val editShowOnboardingCase: EditShowOnboardingCase, - private val analyticsService: AnalyticsService, -) : BaseViewModel() { - - fun onSkipClicked() { - analyticsService.captureEvent(AnalyticsEvent.ONBOARDING_SKIP_CLICK) - markOnboardingAsShown() - } - - fun onStartUsingClicked() { - analyticsService.captureEvent(AnalyticsEvent.ONBOARDING_FINISH_CLICK) - markOnboardingAsShown() - } - - fun onTermsClicked() { - analyticsService.captureEvent(AnalyticsEvent.ONBOARDING_TERMS_CLICK) - } - - fun onNextClicked(position: Int) { - if (position == 0) { - analyticsService.captureEvent(AnalyticsEvent.ONBOARDING_INIT_CLICK) - } - markOnboardingAsShown() - } - - private fun markOnboardingAsShown() { - editShowOnboardingCase(shouldShowOnboarding = false) - } -} \ No newline at end of file diff --git a/start/src/main/java/com/twofasapp/start/ui/onboarding/step/OnboardingStepFragment.kt b/start/src/main/java/com/twofasapp/start/ui/onboarding/step/OnboardingStepFragment.kt deleted file mode 100644 index 03f53551..00000000 --- a/start/src/main/java/com/twofasapp/start/ui/onboarding/step/OnboardingStepFragment.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.twofasapp.start.ui.onboarding.step - -import android.os.Bundle -import android.os.Parcelable -import android.view.View -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import com.twofasapp.base.BaseFragment -import com.twofasapp.start.R -import com.twofasapp.start.databinding.FragmentOnboardingStepBinding -import kotlinx.parcelize.Parcelize - -internal class OnboardingStepFragment : BaseFragment(R.layout.fragment_onboarding_step) { - - companion object { - fun newInstance(params: Params) = OnboardingStepFragment().apply { - arguments = bundleOf("params" to params) - } - } - - private val viewBinding by viewBinding(FragmentOnboardingStepBinding::bind) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val params = requireArguments().getParcelable("params")!! - - viewBinding.title.text = params.title - viewBinding.description.text = params.description - - if (params.imageRes != null) { - viewBinding.image.setImageResource(params.imageRes) - viewBinding.image.isVisible = true - } - - if (params.smallImageRes != null) { - viewBinding.smallImage.setImageResource(params.smallImageRes) - viewBinding.smallImage.isVisible = true - } - } - - @Parcelize - data class Params( - val title: String, - val description: String, - val imageRes: Int? = null, - val smallImageRes: Int? = null, - ) : Parcelable -} \ No newline at end of file diff --git a/start/src/main/java/com/twofasapp/start/ui/start/StartActivity.kt b/start/src/main/java/com/twofasapp/start/ui/start/StartActivity.kt index b590d2d5..58b23601 100644 --- a/start/src/main/java/com/twofasapp/start/ui/start/StartActivity.kt +++ b/start/src/main/java/com/twofasapp/start/ui/start/StartActivity.kt @@ -38,7 +38,7 @@ class StartActivity : AppCompatActivity() { deeplinkHandler.setQueuedDeeplink(incomingData = intent.data?.toString()) if (getShowOnboardingCase()) { - router.navigate(StartDirections.Onboarding) +// router.navigate(StartDirections.Onboarding) } else { authTracker.onSplashScreen() router.navigate(StartDirections.Main) diff --git a/start/src/main/java/com/twofasapp/start/work/OnAppUpdatedWork.kt b/start/src/main/java/com/twofasapp/start/work/OnAppUpdatedWork.kt index 9f7a83c3..ff0101e4 100644 --- a/start/src/main/java/com/twofasapp/start/work/OnAppUpdatedWork.kt +++ b/start/src/main/java/com/twofasapp/start/work/OnAppUpdatedWork.kt @@ -4,8 +4,8 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.twofasapp.base.dispatcher.Dispatchers +import com.twofasapp.common.environment.AppBuild import com.twofasapp.core.analytics.AnalyticsService -import com.twofasapp.environment.AppConfig import com.twofasapp.prefs.usecase.CurrentAppVersionPreference import com.twofasapp.start.domain.ClearObsoletePrefsCase import com.twofasapp.start.domain.MigrateBoxToRoomCase @@ -23,7 +23,7 @@ class OnAppUpdatedWork( private val dispatchers: Dispatchers by inject() private val analyticsService: AnalyticsService by inject() - private val appConfig: AppConfig by inject() + private val appBuild: AppBuild by inject() private val currentAppVersionPreference: CurrentAppVersionPreference by inject() private val clearObsoletePrefsCase: ClearObsoletePrefsCase by inject() private val migratePinCase: MigratePinCase by inject() @@ -33,12 +33,12 @@ class OnAppUpdatedWork( override suspend fun doWork(): Result { return withContext(dispatchers.io()) { try { - if (appConfig.versionCode.toLong() == currentAppVersionPreference.get()) { + if (appBuild.versionCode.toLong() == currentAppVersionPreference.get()) { Timber.d("Migration not needed") return@withContext Result.success() } - Timber.d("Start migration: ${appConfig.versionCode.toLong()} -> ${currentAppVersionPreference.get()}") + Timber.d("Start migration: ${appBuild.versionCode.toLong()} -> ${currentAppVersionPreference.get()}") Timber.d("Migrate: Obsolete prefs") clearObsoletePrefsCase.invoke() @@ -53,7 +53,7 @@ class OnAppUpdatedWork( migratePinCase.invoke() Timber.d("Migration done!") - currentAppVersionPreference.put(appConfig.versionCode.toLong()) + currentAppVersionPreference.put(appBuild.versionCode.toLong()) Result.success() } catch (e: Exception) { diff --git a/start/src/main/res/layout/activity_onboarding.xml b/start/src/main/res/layout/activity_onboarding.xml deleted file mode 100644 index 51a535e1..00000000 --- a/start/src/main/res/layout/activity_onboarding.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/start/src/main/res/layout/fragment_onboarding_step.xml b/start/src/main/res/layout/fragment_onboarding_step.xml deleted file mode 100644 index 593e2b26..00000000 --- a/start/src/main/res/layout/fragment_onboarding_step.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/time/build.gradle.kts b/time/build.gradle.kts index 69580975..dcf72dca 100644 --- a/time/build.gradle.kts +++ b/time/build.gradle.kts @@ -8,10 +8,9 @@ android { } dependencies { - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":time:domain")) implementation(project(":prefs")) - implementation(project(":network")) implementation(project(":truetime")) implementation(project(":truetime-rx")) diff --git a/time/domain/build.gradle.kts b/time/domain/build.gradle.kts index 0cad689a..e5f88842 100644 --- a/time/domain/build.gradle.kts +++ b/time/domain/build.gradle.kts @@ -5,7 +5,4 @@ plugins { android { namespace = "com.twofasapp.time.domain" -} - -dependencies { } \ No newline at end of file diff --git a/widgets/build.gradle.kts b/widgets/build.gradle.kts index e20c9e7c..86ed4cc4 100644 --- a/widgets/build.gradle.kts +++ b/widgets/build.gradle.kts @@ -9,12 +9,12 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":resources")) implementation(project(":extensions")) implementation(project(":prefs")) implementation(project(":design")) - implementation(project(":environment")) + implementation(libs.bundles.appCompat) implementation(libs.bundles.fastAdapter) diff --git a/widgets/domain/build.gradle.kts b/widgets/domain/build.gradle.kts index ea035d4e..a243daf6 100644 --- a/widgets/domain/build.gradle.kts +++ b/widgets/domain/build.gradle.kts @@ -9,15 +9,14 @@ android { dependencies { implementation(project(":base")) - implementation(project(":di")) + implementation(project(":core:di")) implementation(project(":design")) implementation(project(":extensions")) implementation(project(":permissions")) implementation(project(":prefs")) implementation(project(":persistence")) - implementation(project(":network")) implementation(project(":push")) - implementation(project(":environment")) + implementation(libs.bundles.appCompat) implementation(libs.bundles.fastAdapter)