├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── POLICY ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── compose_stability.conf ├── proguard-rules.pro ├── schemas │ └── com.xinto.mauth.db.AccountDatabase │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ ├── 4.json │ │ └── 5.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── xinto │ │ └── mauth │ │ ├── ExampleInstrumentedTest.kt │ │ └── OtpStringTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── xinto │ │ └── mauth │ │ ├── Mauth.kt │ │ ├── core │ │ ├── auth │ │ │ ├── AuthManager.kt │ │ │ └── DefaultAuthManager.kt │ │ ├── camera │ │ │ ├── QrCodeAnalyzer.kt │ │ │ ├── ZxingDecoder.kt │ │ │ └── ZxingEncoder.kt │ │ ├── otp │ │ │ ├── exporter │ │ │ │ ├── DefaultOtpExporter.kt │ │ │ │ └── OtpExporter.kt │ │ │ ├── generator │ │ │ │ ├── DefaultOtpGenerator.kt │ │ │ │ └── OtpGenerator.kt │ │ │ ├── model │ │ │ │ ├── OtpData.kt │ │ │ │ ├── OtpDigest.kt │ │ │ │ └── OtpType.kt │ │ │ ├── parser │ │ │ │ ├── DefaultOtpUriParser.kt │ │ │ │ ├── OtpUriParser.kt │ │ │ │ ├── OtpUriParserError.kt │ │ │ │ └── OtpUriParserResult.kt │ │ │ └── transformer │ │ │ │ ├── DefaultKeyTransformer.kt │ │ │ │ └── KeyTransformer.kt │ │ └── settings │ │ │ ├── DefaultSettings.kt │ │ │ ├── Settings.kt │ │ │ └── model │ │ │ ├── ColorSetting.kt │ │ │ ├── SortSetting.kt │ │ │ └── ThemeSetting.kt │ │ ├── db │ │ ├── AccountDatabase.kt │ │ ├── converter │ │ │ ├── OtpConverter.kt │ │ │ ├── UriConverter.kt │ │ │ └── UuidConverter.kt │ │ └── dao │ │ │ ├── account │ │ │ ├── AccountsDao.kt │ │ │ └── entity │ │ │ │ └── EntityAccount.kt │ │ │ └── rtdata │ │ │ ├── RtdataDao.kt │ │ │ └── entity │ │ │ └── EntityCountData.kt │ │ ├── di │ │ └── MauthDI.kt │ │ ├── domain │ │ ├── AuthRepository.kt │ │ ├── QrRepository.kt │ │ ├── SettingsRepository.kt │ │ ├── account │ │ │ ├── AccountRepository.kt │ │ │ └── model │ │ │ │ ├── DomainAccount.kt │ │ │ │ ├── DomainAccountInfo.kt │ │ │ │ └── DomainExportAccount.kt │ │ └── otp │ │ │ ├── OtpRepository.kt │ │ │ └── model │ │ │ └── DomainOtpRealtimeData.kt │ │ ├── ui │ │ ├── MainActivity.kt │ │ ├── component │ │ │ ├── Biometric.kt │ │ │ ├── ResponsiveAppBarScaffold.kt │ │ │ ├── Shapes.kt │ │ │ ├── TwoPaneCard.kt │ │ │ ├── Uri.kt │ │ │ ├── UriImage.kt │ │ │ ├── form │ │ │ │ ├── ComboBoxFormField.kt │ │ │ │ ├── Form.kt │ │ │ │ ├── FormField.kt │ │ │ │ ├── IntFormField.kt │ │ │ │ ├── LazyForm.kt │ │ │ │ ├── PasswordFormField.kt │ │ │ │ └── TextFormField.kt │ │ │ ├── lazygroup │ │ │ │ ├── GroupedItemHeader.kt │ │ │ │ ├── GroupedItemType.kt │ │ │ │ └── GroupedListItem.kt │ │ │ └── pinboard │ │ │ │ ├── PinBoard.kt │ │ │ │ ├── PinButton.kt │ │ │ │ ├── PinDisplay.kt │ │ │ │ └── PinScaffold.kt │ │ ├── navigation │ │ │ └── MauthDestination.kt │ │ ├── screen │ │ │ ├── about │ │ │ │ ├── AboutLinks.kt │ │ │ │ ├── AboutScreen.kt │ │ │ │ └── component │ │ │ │ │ └── LinkedButtons.kt │ │ │ ├── account │ │ │ │ ├── AccountForm.kt │ │ │ │ ├── AccountScreen.kt │ │ │ │ ├── AccountScreenState.kt │ │ │ │ ├── AccountViewModel.kt │ │ │ │ ├── IconFormField.kt │ │ │ │ ├── component │ │ │ │ │ ├── AccountComboBox.kt │ │ │ │ │ ├── AccountDataField.kt │ │ │ │ │ ├── AccountExitDialog.kt │ │ │ │ │ └── AccountNumberField.kt │ │ │ │ └── state │ │ │ │ │ ├── AccountScreenError.kt │ │ │ │ │ ├── AccountScreenLoading.kt │ │ │ │ │ └── AccountScreenSuccess.kt │ │ │ ├── auth │ │ │ │ ├── AuthScreen.kt │ │ │ │ └── AuthViewModel.kt │ │ │ ├── export │ │ │ │ ├── ExportScreen.kt │ │ │ │ ├── ExportScreenState.kt │ │ │ │ ├── ExportViewModel.kt │ │ │ │ ├── component │ │ │ │ │ └── ZxingQrImage.kt │ │ │ │ └── state │ │ │ │ │ ├── ExportScreenError.kt │ │ │ │ │ ├── ExportScreenLoading.kt │ │ │ │ │ └── ExportScreenSuccess.kt │ │ │ ├── home │ │ │ │ ├── HomeAddAccountMenu.kt │ │ │ │ ├── HomeMoreMenu.kt │ │ │ │ ├── HomeScreen.kt │ │ │ │ ├── HomeScreenState.kt │ │ │ │ ├── HomeViewModel.kt │ │ │ │ ├── component │ │ │ │ │ ├── HomeAccountCard.kt │ │ │ │ │ ├── HomeAddAccountSheet.kt │ │ │ │ │ ├── HomeDeleteAccountsDialog.kt │ │ │ │ │ └── HomeScaffold.kt │ │ │ │ └── state │ │ │ │ │ ├── HomeScreenEmpty.kt │ │ │ │ │ ├── HomeScreenError.kt │ │ │ │ │ ├── HomeScreenLoading.kt │ │ │ │ │ └── HomeScreenSuccess.kt │ │ │ ├── pinremove │ │ │ │ ├── PinRemoveScreen.kt │ │ │ │ ├── PinRemoveScreenState.kt │ │ │ │ └── PinRemoveViewModel.kt │ │ │ ├── pinsetup │ │ │ │ ├── PinSetupScreen.kt │ │ │ │ ├── PinSetupScreenState.kt │ │ │ │ └── PinSetupViewModel.kt │ │ │ ├── qrscan │ │ │ │ ├── QrScanScreen.kt │ │ │ │ ├── QrScanViewModel.kt │ │ │ │ ├── component │ │ │ │ │ ├── QrScanCamera.kt │ │ │ │ │ └── QrScanPermissionDeniedDialog.kt │ │ │ │ └── state │ │ │ │ │ ├── QrScanPermissionDenied.kt │ │ │ │ │ └── QrScanPermissionGranted.kt │ │ │ ├── settings │ │ │ │ ├── SettingsScreen.kt │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ └── component │ │ │ │ │ ├── SettingsItem.kt │ │ │ │ │ ├── SettingsNavigateItem.kt │ │ │ │ │ └── SettingsSwitchItem.kt │ │ │ └── theme │ │ │ │ ├── ThemeScreen.kt │ │ │ │ ├── ThemeViewModel.kt │ │ │ │ └── component │ │ │ │ └── ThemeColorCard.kt │ │ ├── theme │ │ │ ├── Theme.kt │ │ │ ├── Type.kt │ │ │ └── color │ │ │ │ ├── BlueberryBlue.kt │ │ │ │ ├── LemonYellow.kt │ │ │ │ ├── LimeGreen.kt │ │ │ │ ├── MothPurple.kt │ │ │ │ ├── OrangeOrange.kt │ │ │ │ └── SkyCyan.kt │ │ └── util │ │ │ └── FlowProducers.kt │ │ └── util │ │ └── Coroutines.kt │ └── res │ ├── drawable │ ├── ic_add.xml │ ├── ic_add_a_photo.xml │ ├── ic_apartment.xml │ ├── ic_arrow_back.xml │ ├── ic_arrow_downward.xml │ ├── ic_arrow_upward.xml │ ├── ic_backspace.xml │ ├── ic_brush.xml │ ├── ic_bug.xml │ ├── ic_check.xml │ ├── ic_close.xml │ ├── ic_contrast.xml │ ├── ic_copy_all.xml │ ├── ic_delete_forever.xml │ ├── ic_edit.xml │ ├── ic_empty_dashboard.xml │ ├── ic_error.xml │ ├── ic_export.xml │ ├── ic_fingerprint.xml │ ├── ic_github.xml │ ├── ic_info.xml │ ├── ic_key.xml │ ├── ic_keyboard_arrow_down.xml │ ├── ic_label.xml │ ├── ic_launcher_foreground.xml │ ├── ic_moon.xml │ ├── ic_more_vert.xml │ ├── ic_navigate_next.xml │ ├── ic_password.xml │ ├── ic_qr_code_2.xml │ ├── ic_qr_code_scanner.xml │ ├── ic_settings.xml │ ├── ic_sort.xml │ ├── ic_sun.xml │ ├── ic_tab.xml │ ├── ic_undo.xml │ ├── ic_visibility.xml │ └── ic_visibility_off.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── values-fr-rFR │ └── strings.xml │ ├── values-nb-rNO │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-tr-rTR │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── changelogs │ │ ├── 10.txt │ │ ├── 20.txt │ │ ├── 21.txt │ │ ├── 30.txt │ │ ├── 40.txt │ │ ├── 50.txt │ │ ├── 51.txt │ │ ├── 52.txt │ │ ├── 53.txt │ │ ├── 60.txt │ │ ├── 61.txt │ │ ├── 70.txt │ │ ├── 80.txt │ │ └── 90.txt │ ├── full_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 10.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ ├── 7.png │ │ │ ├── 8.png │ │ │ └── 9.png │ ├── short_description.txt │ └── title.txt │ └── ru │ ├── full_description.txt │ └── short_description.txt ├── github ├── get_it_on_github.png └── mauth.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build APK 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: 11 | - '*' 12 | paths-ignore: 13 | - '**.md' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up JDK 17 23 | uses: actions/setup-java@v2 24 | with: 25 | java-version: 17 26 | distribution: 'zulu' 27 | cache: 'gradle' 28 | 29 | - name: chmod gradlew 30 | run: chmod +x gradlew 31 | 32 | - name: Build the APK 33 | run: ./gradlew assembleDebug 34 | 35 | - name: Upload the APK 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: mauth 39 | path: app/build/outputs/apk/debug/app-debug.apk -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .DS_Store 5 | build 6 | captures 7 | .externalNativeBuild 8 | .cxx 9 | local.properties 10 | app/release -------------------------------------------------------------------------------- /POLICY: -------------------------------------------------------------------------------- 1 | Privacy Policy 2 | This privacy policy applies to the Mauth app (hereby referred to as "Application") for mobile devices that was created by Tornike Khintibidze (hereby referred to as "Service Provider") as an Open Source service. This service is intended for use "AS IS". 3 | 4 | What information does the Application obtain and how is it used? 5 | The Application does not obtain any information when you download and use it. Registration is not required to use the Application. 6 | 7 | Does the Application collect precise real time location information of the device? 8 | This Application does not collect precise information about the location of your mobile device. 9 | 10 | Do third parties see and/or have access to information obtained by the Application? 11 | Since the Application does not collect any information, no data is shared with third parties. 12 | 13 | What are my opt-out rights? 14 | You can stop all collection of information by the Application easily by uninstalling it. You may use the standard uninstall processes as may be available as part of your mobile device or via the mobile application marketplace or network. 15 | 16 | Children 17 | The Application is not used to knowingly solicit data from or market to children under the age of 13. 18 | 19 | The Service Provider does not knowingly collect personally identifiable information from children. The Service Provider encourages all children to never submit any personally identifiable information through the Application and/or Services. The Service Provider encourage parents and legal guardians to monitor their children's Internet usage and to help enforce this Policy by instructing their children never to provide personally identifiable information through the Application and/or Services without their permission. If you have reason to believe that a child has provided personally identifiable information to the Service Provider through the Application and/or Services, please contact the Service Provider (tornike.khintibidze2@gmail.com) so that they will be able to take the necessary actions. You must also be at least 16 years of age to consent to the processing of your personally identifiable information in your country (in some countries we may allow your parent or guardian to do so on your behalf). 20 | 21 | Security 22 | The Service Provider is concerned about safeguarding the confidentiality of your information. However, since the Application does not collect any information, there is no risk of your data being accessed by unauthorized individuals. 23 | 24 | Changes 25 | This Privacy Policy may be updated from time to time for any reason. The Service Provider will notify you of any changes to their Privacy Policy by updating this page with the new Privacy Policy. You are advised to consult this Privacy Policy regularly for any changes, as continued use is deemed approval of all changes. 26 | 27 | This privacy policy is effective as of 2025-01-04 28 | 29 | Your Consent 30 | By using the Application, you are consenting to the processing of your information as set forth in this Privacy Policy now and as amended by the Service Provider. 31 | 32 | Contact Us 33 | If you have any questions regarding privacy while using the Application, or have questions about the practices, please contact the Service Provider via email at tornike.khintibidze2@gmail.com. 34 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/compose_stability.conf: -------------------------------------------------------------------------------- 1 | kotlin.enums.EnumEntries -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontwarn com.google.errorprone.annotations.Immutable 24 | -dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue 25 | -dontwarn com.google.errorprone.annotations.CheckReturnValue 26 | -dontwarn com.google.errorprone.annotations.RestrictedApi -------------------------------------------------------------------------------- /app/schemas/com.xinto.mauth.db.AccountDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "a7b633760ea5ecaa908edaf929f167eb", 6 | "entities": [ 7 | { 8 | "tableName": "accounts", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `secret` TEXT NOT NULL, `label` TEXT NOT NULL, `issuer` TEXT NOT NULL, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "BLOB", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "secret", 19 | "columnName": "secret", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "label", 25 | "columnName": "label", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "issuer", 31 | "columnName": "issuer", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | } 35 | ], 36 | "primaryKey": { 37 | "columnNames": [ 38 | "id" 39 | ], 40 | "autoGenerate": false 41 | }, 42 | "indices": [], 43 | "foreignKeys": [] 44 | } 45 | ], 46 | "views": [], 47 | "setupQueries": [ 48 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 49 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a7b633760ea5ecaa908edaf929f167eb')" 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /app/schemas/com.xinto.mauth.db.AccountDatabase/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 2, 5 | "identityHash": "b71f0ec08f1e3c91ebda8a8bda8ddd40", 6 | "entities": [ 7 | { 8 | "tableName": "accounts", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `secret` TEXT NOT NULL, `label` TEXT NOT NULL, `issuer` TEXT NOT NULL, `algorithm` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 0, `digits` INTEGER NOT NULL DEFAULT 6, `counter` INTEGER NOT NULL DEFAULT 0, `period` INTEGER NOT NULL DEFAULT 30, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "BLOB", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "secret", 19 | "columnName": "secret", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "label", 25 | "columnName": "label", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "issuer", 31 | "columnName": "issuer", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "algorithm", 37 | "columnName": "algorithm", 38 | "affinity": "INTEGER", 39 | "notNull": true, 40 | "defaultValue": "0" 41 | }, 42 | { 43 | "fieldPath": "type", 44 | "columnName": "type", 45 | "affinity": "INTEGER", 46 | "notNull": true, 47 | "defaultValue": "0" 48 | }, 49 | { 50 | "fieldPath": "digits", 51 | "columnName": "digits", 52 | "affinity": "INTEGER", 53 | "notNull": true, 54 | "defaultValue": "6" 55 | }, 56 | { 57 | "fieldPath": "counter", 58 | "columnName": "counter", 59 | "affinity": "INTEGER", 60 | "notNull": true, 61 | "defaultValue": "0" 62 | }, 63 | { 64 | "fieldPath": "period", 65 | "columnName": "period", 66 | "affinity": "INTEGER", 67 | "notNull": true, 68 | "defaultValue": "30" 69 | } 70 | ], 71 | "primaryKey": { 72 | "columnNames": [ 73 | "id" 74 | ], 75 | "autoGenerate": false 76 | }, 77 | "indices": [], 78 | "foreignKeys": [] 79 | } 80 | ], 81 | "views": [], 82 | "setupQueries": [ 83 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 84 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b71f0ec08f1e3c91ebda8a8bda8ddd40')" 85 | ] 86 | } 87 | } -------------------------------------------------------------------------------- /app/schemas/com.xinto.mauth.db.AccountDatabase/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 3, 5 | "identityHash": "9c1e38cb1cec8c1fd74fa9a90eb7b79c", 6 | "entities": [ 7 | { 8 | "tableName": "accounts", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `icon` TEXT, `secret` TEXT NOT NULL, `label` TEXT NOT NULL, `issuer` TEXT NOT NULL, `algorithm` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 0, `digits` INTEGER NOT NULL DEFAULT 6, `counter` INTEGER NOT NULL DEFAULT 0, `period` INTEGER NOT NULL DEFAULT 30, PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "BLOB", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "icon", 19 | "columnName": "icon", 20 | "affinity": "TEXT", 21 | "notNull": false 22 | }, 23 | { 24 | "fieldPath": "secret", 25 | "columnName": "secret", 26 | "affinity": "TEXT", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "label", 31 | "columnName": "label", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | }, 35 | { 36 | "fieldPath": "issuer", 37 | "columnName": "issuer", 38 | "affinity": "TEXT", 39 | "notNull": true 40 | }, 41 | { 42 | "fieldPath": "algorithm", 43 | "columnName": "algorithm", 44 | "affinity": "INTEGER", 45 | "notNull": true, 46 | "defaultValue": "0" 47 | }, 48 | { 49 | "fieldPath": "type", 50 | "columnName": "type", 51 | "affinity": "INTEGER", 52 | "notNull": true, 53 | "defaultValue": "0" 54 | }, 55 | { 56 | "fieldPath": "digits", 57 | "columnName": "digits", 58 | "affinity": "INTEGER", 59 | "notNull": true, 60 | "defaultValue": "6" 61 | }, 62 | { 63 | "fieldPath": "counter", 64 | "columnName": "counter", 65 | "affinity": "INTEGER", 66 | "notNull": true, 67 | "defaultValue": "0" 68 | }, 69 | { 70 | "fieldPath": "period", 71 | "columnName": "period", 72 | "affinity": "INTEGER", 73 | "notNull": true, 74 | "defaultValue": "30" 75 | } 76 | ], 77 | "primaryKey": { 78 | "columnNames": [ 79 | "id" 80 | ], 81 | "autoGenerate": false 82 | }, 83 | "indices": [], 84 | "foreignKeys": [] 85 | } 86 | ], 87 | "views": [], 88 | "setupQueries": [ 89 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 90 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c1e38cb1cec8c1fd74fa9a90eb7b79c')" 91 | ] 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/xinto/mauth/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.xinto.mauth", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/xinto/mauth/OtpStringTest.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import com.xinto.mauth.core.otp.model.OtpDigest 5 | import com.xinto.mauth.core.otp.parser.DefaultOtpUriParser 6 | import com.xinto.mauth.core.otp.parser.OtpUriParserResult 7 | import org.junit.Assert 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | @RunWith(AndroidJUnit4::class) 12 | class OtpStringTest { 13 | 14 | private val parser = DefaultOtpUriParser() 15 | private val testString = "otpauth://totp/account?secret=secret&issuer=issuer&algorithm=sha1&digits=6&period=30" 16 | 17 | @Test 18 | fun testStringValidation() { 19 | val parseResult = parser.parseOtpUri(testString) 20 | Assert.assertTrue(parseResult.toString(), parseResult is OtpUriParserResult.Success) 21 | 22 | val castResult = parseResult as OtpUriParserResult.Success 23 | Assert.assertTrue(castResult.data.label == "account") 24 | Assert.assertTrue(castResult.data.secret == "secret") 25 | Assert.assertTrue(castResult.data.issuer == "issuer") 26 | Assert.assertTrue(castResult.data.algorithm == OtpDigest.SHA1) 27 | Assert.assertTrue(castResult.data.digits ==6) 28 | Assert.assertTrue(castResult.data.period ==30) 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/Mauth.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth 2 | 3 | import android.app.Application 4 | import com.xinto.mauth.di.MauthDI 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.core.context.startKoin 7 | 8 | class Mauth : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | startKoin { 14 | androidContext(this@Mauth) 15 | 16 | modules(MauthDI.all) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/auth/AuthManager.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.auth 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface AuthManager { 6 | 7 | fun getCode(): Flow 8 | 9 | fun setCode(code: String) 10 | 11 | fun removeCode() 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/auth/DefaultAuthManager.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.auth 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import androidx.core.content.edit 6 | import androidx.security.crypto.EncryptedSharedPreferences 7 | import androidx.security.crypto.MasterKey 8 | import kotlinx.coroutines.channels.awaitClose 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.callbackFlow 11 | 12 | class DefaultAuthManager( 13 | context: Context 14 | ) : AuthManager { 15 | 16 | private val prefs = EncryptedSharedPreferences.create( 17 | context, 18 | "auth", 19 | MasterKey(context = context), 20 | EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, 21 | EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, 22 | ) 23 | 24 | override fun getCode(): Flow { 25 | return callbackFlow { 26 | val listener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> 27 | if (key == KEY_CODE) { 28 | trySend(sharedPreferences.getString(KEY_CODE, null)) 29 | } 30 | } 31 | send(prefs.getString(KEY_CODE, null)) 32 | prefs.registerOnSharedPreferenceChangeListener(listener) 33 | awaitClose { 34 | prefs.unregisterOnSharedPreferenceChangeListener(listener) 35 | } 36 | } 37 | } 38 | 39 | override fun setCode(code: String) { 40 | prefs.edit { 41 | putString(KEY_CODE, code) 42 | } 43 | } 44 | 45 | override fun removeCode() { 46 | prefs.edit { 47 | remove(KEY_CODE) 48 | } 49 | } 50 | 51 | private companion object { 52 | const val KEY_CODE = "code" 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/camera/QrCodeAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.camera 2 | 3 | import androidx.camera.core.ImageAnalysis 4 | import androidx.camera.core.ImageProxy 5 | import com.google.zxing.NotFoundException 6 | import java.nio.ByteBuffer 7 | 8 | class QrCodeAnalyzer( 9 | private inline val onSuccess: (com.google.zxing.Result) -> Unit, 10 | private inline val onFail: (NotFoundException) -> Unit 11 | ) : ImageAnalysis.Analyzer { 12 | 13 | override fun analyze(image: ImageProxy) { 14 | image.use { imageProxy -> 15 | val data = imageProxy.planes[0].buffer.toByteArray() 16 | 17 | ZxingDecoder.decodeYuvLuminanceSource( 18 | data = data, 19 | dataWidth = imageProxy.width, 20 | dataHeight = imageProxy.height, 21 | onSuccess = onSuccess, 22 | onError = onFail 23 | ) 24 | } 25 | } 26 | 27 | private fun ByteBuffer.toByteArray(): ByteArray { 28 | rewind() 29 | val bytes = ByteArray(remaining()) 30 | get(bytes) 31 | return bytes 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/camera/ZxingDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.camera 2 | 3 | import com.google.zxing.BarcodeFormat 4 | import com.google.zxing.BinaryBitmap 5 | import com.google.zxing.DecodeHintType 6 | import com.google.zxing.LuminanceSource 7 | import com.google.zxing.MultiFormatReader 8 | import com.google.zxing.NotFoundException 9 | import com.google.zxing.PlanarYUVLuminanceSource 10 | import com.google.zxing.RGBLuminanceSource 11 | import com.google.zxing.Result 12 | import com.google.zxing.common.HybridBinarizer 13 | 14 | object ZxingDecoder { 15 | 16 | val reader = MultiFormatReader().apply { 17 | setHints( 18 | mapOf( 19 | DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE), 20 | ) 21 | ) 22 | } 23 | 24 | inline fun decodeYuvLuminanceSource( 25 | data: ByteArray, 26 | dataWidth: Int, 27 | dataHeight: Int, 28 | width: Int = dataWidth, 29 | height: Int = dataHeight, 30 | left: Int = 0, 31 | top: Int = 0, 32 | reverseHorizontal: Boolean = false, 33 | onSuccess: (Result) -> T, 34 | onError: (NotFoundException) -> T 35 | ): T { 36 | val source = PlanarYUVLuminanceSource( 37 | /* yuvData = */ 38 | data, 39 | /* dataWidth = */ 40 | dataWidth, 41 | /* dataHeight = */ 42 | dataHeight, 43 | /* left = */ 44 | left, 45 | /* top = */ 46 | top, 47 | /* width = */ 48 | width, 49 | /* height = */ 50 | height, 51 | /* reverseHorizontal = */ 52 | reverseHorizontal, 53 | ) 54 | 55 | return decodeSource(source, onSuccess, onError) 56 | } 57 | 58 | inline fun decodeRgbLuminanceSource( 59 | pixels: IntArray, 60 | width: Int, 61 | height: Int, 62 | onSuccess: (Result) -> T, 63 | onError: (NotFoundException) -> T 64 | ): T { 65 | val source = RGBLuminanceSource( 66 | /* width = */ 67 | width, 68 | /* height = */ 69 | height, 70 | /* pixels = */ 71 | pixels, 72 | ) 73 | 74 | return decodeSource(source, onSuccess, onError) 75 | } 76 | 77 | inline fun decodeSource( 78 | source: LuminanceSource, 79 | onSuccess: (Result) -> T, 80 | onError: (NotFoundException) -> T 81 | ): T { 82 | val bitmap = BinaryBitmap(HybridBinarizer(source)) 83 | 84 | return try { 85 | onSuccess(reader.decodeWithState(bitmap)) 86 | } catch (e: NotFoundException) { 87 | onError(e) 88 | } 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/camera/ZxingEncoder.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.camera 2 | 3 | import android.graphics.Bitmap 4 | import androidx.annotation.ColorInt 5 | import com.google.zxing.BarcodeFormat 6 | import com.google.zxing.EncodeHintType 7 | import com.google.zxing.MultiFormatWriter 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import kotlin.coroutines.resume 11 | import kotlin.coroutines.suspendCoroutine 12 | 13 | object ZxingEncoder { 14 | 15 | private val writer = MultiFormatWriter() 16 | 17 | suspend fun encodeToBitmap( 18 | data: String, 19 | size: Int, 20 | @ColorInt backgroundColor: Int, 21 | @ColorInt dataColor: Int 22 | ): Bitmap { 23 | return withContext(Dispatchers.IO){ 24 | suspendCoroutine { continuation -> 25 | val bitMatrix = writer.encode( 26 | /* contents = */ data, 27 | /* format = */ BarcodeFormat.QR_CODE, 28 | /* width = */ size, 29 | /* height = */ size, 30 | /* hints = */ mapOf(EncodeHintType.MARGIN to 2) 31 | ) 32 | val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply { 33 | for (x in 0 until size) { 34 | for (y in 0 until size) { 35 | val hasData = bitMatrix.get(x, y) 36 | setPixel(x, y, if (hasData) dataColor else backgroundColor) 37 | } 38 | } 39 | } 40 | continuation.resume(bitmap) 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/exporter/DefaultOtpExporter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.exporter 2 | 3 | import android.net.Uri 4 | import com.xinto.mauth.core.otp.model.OtpData 5 | import com.xinto.mauth.core.otp.model.OtpType 6 | 7 | class DefaultOtpExporter : OtpExporter { 8 | 9 | override fun exportOtp(data: OtpData): String { 10 | val uriBuilder = Uri.Builder() 11 | .scheme("otpauth") 12 | .appendPath(data.label) 13 | .appendQueryParameter("secret", data.secret) 14 | .appendQueryParameter("algorithm", data.algorithm.name) 15 | .appendQueryParameter("digits", data.digits.toString()) 16 | 17 | if (data.issuer.isNotBlank()) { 18 | uriBuilder.appendQueryParameter("issuer", data.issuer) 19 | } 20 | 21 | return when (data.type) { 22 | OtpType.TOTP -> { 23 | uriBuilder 24 | .authority("totp") 25 | .appendQueryParameter("period", data.period.toString()) 26 | } 27 | OtpType.HOTP -> { 28 | uriBuilder 29 | .authority("hotp") 30 | .appendQueryParameter("counter", data.period.toString()) 31 | } 32 | }.toString().also(::println) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/exporter/OtpExporter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.exporter 2 | 3 | import com.xinto.mauth.core.otp.model.OtpData 4 | 5 | interface OtpExporter { 6 | 7 | fun exportOtp(data: OtpData): String 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/generator/DefaultOtpGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.generator 2 | 3 | import com.xinto.mauth.core.otp.model.OtpDigest 4 | import java.nio.ByteBuffer 5 | import javax.crypto.Mac 6 | import javax.crypto.spec.SecretKeySpec 7 | import kotlin.math.floor 8 | import kotlin.math.pow 9 | 10 | class DefaultOtpGenerator : OtpGenerator { 11 | 12 | override fun generateHotp( 13 | secret: ByteArray, 14 | counter: Long, 15 | digits: Int, 16 | digest: OtpDigest 17 | ): String { 18 | val hash = Mac.getInstance(digest.algorithmName).let { mac -> 19 | val byteCounter = ByteBuffer.allocate(8) 20 | .putLong(counter) 21 | .array() 22 | 23 | mac.init(SecretKeySpec(secret, "RAW")) 24 | mac.doFinal(byteCounter) 25 | } 26 | 27 | val offset = hash[hash.size - 1].toInt() and 0xF 28 | 29 | val code = ((hash[offset].toInt() and 0x7f) shl 24) or 30 | ((hash[offset + 1].toInt() and 0xff) shl 16) or 31 | ((hash[offset + 2].toInt() and 0xff) shl 8) or 32 | ((hash[offset + 3].toInt() and 0xff)) 33 | 34 | val paddedCode = (code % 10.0.pow(digits.toDouble())).toInt() 35 | 36 | return StringBuilder(paddedCode.toString()).apply { 37 | while (length < digits) { 38 | insert(0, "0") 39 | } 40 | }.toString() 41 | } 42 | 43 | override fun generateTotp( 44 | secret: ByteArray, 45 | interval: Long, 46 | seconds: Long, 47 | digits: Int, 48 | digest: OtpDigest 49 | ): String { 50 | val counter = floor((seconds / interval).toDouble()).toLong() 51 | return generateHotp(secret, counter, digits, digest) 52 | } 53 | 54 | private val OtpDigest.algorithmName: String 55 | get() { 56 | return when (this) { 57 | OtpDigest.SHA1 -> "HmacSHA1" 58 | OtpDigest.SHA256 -> "HmacSHA256" 59 | OtpDigest.SHA512 -> "HmacSHA512" 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/generator/OtpGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.generator 2 | 3 | import com.xinto.mauth.core.otp.model.OtpDigest 4 | 5 | interface OtpGenerator { 6 | 7 | fun generateHotp( 8 | secret: ByteArray, 9 | counter: Long, 10 | digits: Int = 6, 11 | digest: OtpDigest = OtpDigest.SHA1 12 | ): String 13 | 14 | fun generateTotp( 15 | secret: ByteArray, 16 | interval: Long, 17 | seconds: Long, 18 | digits: Int = 6, 19 | digest: OtpDigest = OtpDigest.SHA1 20 | ): String 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/model/OtpData.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.model 2 | 3 | data class OtpData( 4 | val label: String, 5 | val issuer: String, 6 | val secret: String, 7 | val algorithm: OtpDigest, 8 | val type: OtpType, 9 | val digits: Int, 10 | val counter: Int?, 11 | val period: Int?, 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/model/OtpDigest.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.model 2 | 3 | enum class OtpDigest { 4 | SHA1, 5 | SHA256, 6 | SHA512 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/model/OtpType.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.model 2 | 3 | enum class OtpType { 4 | HOTP, 5 | TOTP, 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/parser/DefaultOtpUriParser.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.parser 2 | 3 | import android.net.Uri 4 | import com.xinto.mauth.core.otp.model.OtpData 5 | import com.xinto.mauth.core.otp.model.OtpDigest 6 | import com.xinto.mauth.core.otp.model.OtpType 7 | 8 | 9 | class DefaultOtpUriParser : OtpUriParser { 10 | 11 | override fun parseOtpUri(keyUri: String): OtpUriParserResult { 12 | val uri = Uri.parse(keyUri) 13 | 14 | val protocol = uri.scheme?.lowercase() 15 | if (protocol != "otpauth") { 16 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_PROTOCOL) 17 | } 18 | 19 | val type = when (uri.host?.lowercase()) { 20 | "hotp" -> OtpType.HOTP 21 | "totp" -> OtpType.TOTP 22 | else -> return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_TYPE) 23 | } 24 | 25 | val label = try { 26 | uri.pathSegments[0] 27 | } catch (e: IndexOutOfBoundsException) { 28 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_MISSING_LABEL) 29 | } 30 | 31 | val paramSecret = uri.getQueryParameter("secret") 32 | ?: return OtpUriParserResult.Failure(OtpUriParserError.ERROR_MISSING_SECRET) 33 | 34 | val paramIssuer = uri.getQueryParameter("issuer") ?: "" 35 | 36 | val paramAlgorithm = uri.getQueryParameter("algorithm") ?: "SHA1" 37 | val algorithm = getDigestFromUriAlgorithm(paramAlgorithm) 38 | ?: return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_ALGORITHM) 39 | 40 | val paramDigits = uri.getQueryParameter("digits") ?: "6" 41 | val digits = paramDigits.toIntOrNull() 42 | ?: return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_DIGITS) 43 | 44 | val paramPeriod = uri.getQueryParameter("period") ?: "30" 45 | val period = try { 46 | if (type == OtpType.HOTP) null else paramPeriod.toInt() 47 | } catch (e: NumberFormatException) { 48 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_PERIOD) 49 | } 50 | 51 | val paramCounter = uri.getQueryParameter("counter") 52 | if (type == OtpType.HOTP && paramCounter == null) { 53 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_MISSING_COUNTER) 54 | } 55 | val counter = try { 56 | paramCounter?.toInt() 57 | } catch (e: NumberFormatException) { 58 | return OtpUriParserResult.Failure(OtpUriParserError.ERROR_INVALID_COUNTER) 59 | } 60 | 61 | val otpData = OtpData( 62 | label = label, 63 | issuer = paramIssuer, 64 | secret = paramSecret, 65 | algorithm = algorithm, 66 | type = type, 67 | digits = digits, 68 | period = period, 69 | counter = counter, 70 | ) 71 | 72 | return OtpUriParserResult.Success(otpData) 73 | } 74 | 75 | private fun getDigestFromUriAlgorithm(algorithm: String): OtpDigest? { 76 | return when (algorithm.uppercase()) { 77 | "SHA1" -> OtpDigest.SHA1 78 | "SHA256" -> OtpDigest.SHA256 79 | "SHA512" -> OtpDigest.SHA512 80 | else -> null 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/parser/OtpUriParser.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.parser 2 | 3 | interface OtpUriParser { 4 | fun parseOtpUri(keyUri: String): OtpUriParserResult 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/parser/OtpUriParserError.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.parser 2 | 3 | enum class OtpUriParserError { 4 | ERROR_INVALID_PROTOCOL, 5 | ERROR_INVALID_TYPE, 6 | ERROR_INVALID_ALGORITHM, 7 | ERROR_INVALID_DIGITS, 8 | ERROR_INVALID_PERIOD, 9 | ERROR_INVALID_COUNTER, 10 | ERROR_MISSING_LABEL, 11 | ERROR_MISSING_SECRET, 12 | ERROR_MISSING_COUNTER, 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/parser/OtpUriParserResult.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.parser 2 | 3 | import com.xinto.mauth.core.otp.model.OtpData 4 | 5 | sealed interface OtpUriParserResult { 6 | data class Success(val data: OtpData) : OtpUriParserResult 7 | data class Failure(val error: OtpUriParserError) : OtpUriParserResult 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/transformer/DefaultKeyTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.transformer 2 | 3 | import org.apache.commons.codec.binary.Base32 4 | 5 | class DefaultKeyTransformer : KeyTransformer { 6 | 7 | private val base32 = Base32() 8 | 9 | override fun transformToBytes(key: String): ByteArray { 10 | val trimmed = key.trim() 11 | .replace("-", "") 12 | .replace(" ", "") 13 | return base32.decode(trimmed) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/otp/transformer/KeyTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.otp.transformer 2 | 3 | interface KeyTransformer { 4 | 5 | fun transformToBytes(key: String): ByteArray 6 | 7 | } 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/settings/DefaultSettings.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.settings 2 | 3 | import android.content.Context 4 | import androidx.datastore.preferences.core.booleanPreferencesKey 5 | import androidx.datastore.preferences.core.edit 6 | import androidx.datastore.preferences.core.stringPreferencesKey 7 | import androidx.datastore.preferences.preferencesDataStore 8 | import com.xinto.mauth.core.settings.model.ColorSetting 9 | import com.xinto.mauth.core.settings.model.SortSetting 10 | import com.xinto.mauth.core.settings.model.ThemeSetting 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.map 13 | 14 | class DefaultSettings(context: Context) : Settings { 15 | 16 | private val Context.preferences by preferencesDataStore("preferences") 17 | private val preferences = context.preferences 18 | 19 | override fun getSecureMode(): Flow { 20 | return preferences.data.map { 21 | it[KEY_SECURE_MODE] ?: false 22 | } 23 | } 24 | 25 | override fun getUseBiometrics(): Flow { 26 | return preferences.data.map { 27 | it[KEY_USE_BIOMETRICS] ?: false 28 | } 29 | } 30 | 31 | override fun getSortMode(): Flow { 32 | return preferences.data.map { 33 | it[KEY_SORT_MODE]?.let { name -> 34 | SortSetting.valueOf(name) 35 | } ?: SortSetting.DEFAULT 36 | } 37 | } 38 | 39 | override fun getTheme(): Flow { 40 | return preferences.data.map { preferences -> 41 | preferences[KEY_THEME]?.let { name -> 42 | ThemeSetting.entries.find { it.name == name } 43 | } ?: ThemeSetting.DEFAULT 44 | } 45 | } 46 | 47 | override fun getColor(): Flow { 48 | return preferences.data.map { preferences -> 49 | preferences[KEY_COLOR]?.let { name -> 50 | ColorSetting.entries.find { it.name == name } 51 | } ?: ColorSetting.DEFAULT 52 | } 53 | } 54 | 55 | override suspend fun setSecureMode(value: Boolean) { 56 | preferences.edit { 57 | it[KEY_SECURE_MODE] = value 58 | } 59 | } 60 | 61 | override suspend fun setUseBiometrics(value: Boolean) { 62 | preferences.edit { 63 | it[KEY_USE_BIOMETRICS] = value 64 | } 65 | } 66 | 67 | override suspend fun setSortMode(value: SortSetting) { 68 | preferences.edit { 69 | it[KEY_SORT_MODE] = value.name 70 | } 71 | } 72 | 73 | override suspend fun setTheme(value: ThemeSetting) { 74 | preferences.edit { 75 | it[KEY_THEME] = value.name 76 | } 77 | } 78 | 79 | override suspend fun setColor(value: ColorSetting) { 80 | preferences.edit { 81 | it[KEY_COLOR] = value.name 82 | } 83 | } 84 | 85 | private companion object { 86 | val KEY_SECURE_MODE = booleanPreferencesKey("private_mode") 87 | val KEY_USE_BIOMETRICS = booleanPreferencesKey("use_biometrics") 88 | val KEY_SORT_MODE = stringPreferencesKey("sort_mode") 89 | val KEY_THEME = stringPreferencesKey("theme") 90 | val KEY_COLOR = stringPreferencesKey("color") 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/settings/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.settings 2 | 3 | import com.xinto.mauth.core.settings.model.ColorSetting 4 | import com.xinto.mauth.core.settings.model.SortSetting 5 | import com.xinto.mauth.core.settings.model.ThemeSetting 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface Settings { 9 | fun getSecureMode(): Flow 10 | fun getUseBiometrics(): Flow 11 | fun getSortMode(): Flow 12 | fun getTheme(): Flow 13 | fun getColor(): Flow 14 | 15 | suspend fun setSecureMode(value: Boolean) 16 | suspend fun setUseBiometrics(value: Boolean) 17 | suspend fun setSortMode(value: SortSetting) 18 | suspend fun setTheme(value: ThemeSetting) 19 | suspend fun setColor(value: ColorSetting) 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/settings/model/ColorSetting.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.settings.model 2 | 3 | import android.os.Build 4 | 5 | enum class ColorSetting { 6 | Dynamic, 7 | MothPurple, 8 | BlueberryBlue, 9 | PickleYellow, 10 | ToxicGreen, 11 | LeatherOrange, 12 | OceanTurquoise; 13 | 14 | companion object { 15 | val DEFAULT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Dynamic else MothPurple 16 | 17 | val validEntries = entries.filter { 18 | it != Dynamic || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/settings/model/SortSetting.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.settings.model 2 | 3 | enum class SortSetting { 4 | DateAsc, 5 | DateDesc, 6 | LabelAsc, 7 | LabelDesc, 8 | IssuerAsc, 9 | IssuerDesc; 10 | 11 | companion object { 12 | val DEFAULT = DateDesc 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/core/settings/model/ThemeSetting.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.core.settings.model 2 | 3 | enum class ThemeSetting { 4 | System, 5 | Dark, 6 | Light; 7 | 8 | companion object { 9 | val DEFAULT = System 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/AccountDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db 2 | 3 | import androidx.room.AutoMigration 4 | import androidx.room.Database 5 | import androidx.room.RoomDatabase 6 | import androidx.room.TypeConverters 7 | import androidx.room.migration.Migration 8 | import androidx.sqlite.db.SupportSQLiteDatabase 9 | import com.xinto.mauth.db.converter.OtpConverter 10 | import com.xinto.mauth.db.converter.UriConverter 11 | import com.xinto.mauth.db.converter.UuidConverter 12 | import com.xinto.mauth.db.dao.account.AccountsDao 13 | import com.xinto.mauth.db.dao.account.entity.EntityAccount 14 | import com.xinto.mauth.db.dao.rtdata.RtdataDao 15 | import com.xinto.mauth.db.dao.rtdata.entity.EntityCountData 16 | 17 | @Database( 18 | entities = [EntityAccount::class, EntityCountData::class], 19 | version = 5, 20 | autoMigrations = [ 21 | AutoMigration( 22 | from = 1, 23 | to = 2 24 | ), 25 | AutoMigration( 26 | from = 2, 27 | to = 3 28 | ), 29 | AutoMigration( 30 | from = 4, 31 | to = 5 32 | ) 33 | ] 34 | ) 35 | @TypeConverters(UuidConverter::class, OtpConverter::class, UriConverter::class) 36 | abstract class AccountDatabase : RoomDatabase() { 37 | 38 | abstract fun accountsDao(): AccountsDao 39 | 40 | abstract fun rtdataDao(): RtdataDao 41 | 42 | companion object Migrations { 43 | 44 | val Migrate3to4 = object : Migration(3, 4) { 45 | override fun migrate(database: SupportSQLiteDatabase) { 46 | database.execSQL("CREATE TABLE countdata (account_id BLOB NOT NULL, count INTEGER NOT NULL, PRIMARY KEY(account_id))") 47 | database.execSQL("INSERT INTO countdata (account_id, count) SELECT id, counter FROM accounts") 48 | database.execSQL("CREATE TABLE accounts_temp (id BLOB NOT NULL, icon TEXT, secret TEXT NOT NULL, label TEXT NOT NULL, issuer TEXT NOT NULL, algorithm INTEGER NOT NULL DEFAULT 0, type INTEGER NOT NULL DEFAULT 0, digits INTEGER NOT NULL DEFAULT 6, period INTEGER NOT NULL DEFAULT 30, PRIMARY KEY(id))") 49 | database.execSQL("INSERT INTO accounts_temp (id, icon, secret, label, issuer, algorithm, type, digits, period) SELECT id, icon, secret, label, issuer, algorithm, type, digits, period FROM accounts") 50 | database.execSQL("DROP TABLE accounts") 51 | database.execSQL("ALTER TABLE accounts_temp RENAME TO accounts") 52 | } 53 | } 54 | 55 | val Migrate4To5 = object : Migration(4, 5) { 56 | override fun migrate(database: SupportSQLiteDatabase) { 57 | database.execSQL("ALTER TABLE accounts ADD COLUMN create_date INTEGER NOT NULL DEFAULT 0") 58 | database.execSQL("UPDATE accounts SET create_date = strftime('%s','now') + ROWID") 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/converter/OtpConverter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db.converter 2 | 3 | import androidx.room.TypeConverter 4 | import com.xinto.mauth.core.otp.model.OtpDigest 5 | import com.xinto.mauth.core.otp.model.OtpType 6 | 7 | class OtpConverter { 8 | 9 | @TypeConverter 10 | fun fromIntToDigest(value: Int): OtpDigest { 11 | return OtpDigest.values()[value] 12 | } 13 | 14 | @TypeConverter 15 | fun fromDigestToInt(digest: OtpDigest): Int { 16 | return digest.ordinal 17 | } 18 | 19 | @TypeConverter 20 | fun fromIntToType(value: Int): OtpType { 21 | return OtpType.values()[value] 22 | } 23 | 24 | @TypeConverter 25 | fun fromTypeToInt(digest: OtpType): Int { 26 | return digest.ordinal 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/converter/UriConverter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db.converter 2 | 3 | import android.net.Uri 4 | import androidx.room.TypeConverter 5 | 6 | class UriConverter { 7 | 8 | @TypeConverter 9 | fun convertUriToString(uri: Uri): String { 10 | return uri.toString() 11 | } 12 | 13 | @TypeConverter 14 | fun convertStringToUri(uriString: String): Uri { 15 | return Uri.parse(uriString) 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/converter/UuidConverter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db.converter 2 | 3 | import androidx.room.TypeConverter 4 | import java.nio.ByteBuffer 5 | import java.nio.ByteOrder 6 | import java.util.UUID 7 | 8 | class UuidConverter { 9 | 10 | @TypeConverter 11 | fun uuidToBytes(uuid: UUID): ByteArray { 12 | val bytes = ByteArray(16) 13 | 14 | ByteBuffer.wrap(bytes) 15 | .order(ByteOrder.BIG_ENDIAN) 16 | .putLong(uuid.mostSignificantBits) 17 | .putLong(uuid.leastSignificantBits) 18 | 19 | return bytes 20 | } 21 | 22 | @TypeConverter 23 | fun bytesToUuid(uuid: ByteArray): UUID { 24 | val buffer = ByteBuffer.wrap(uuid) 25 | 26 | return UUID(buffer.long, buffer.long) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/dao/account/AccountsDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db.dao.account 2 | 3 | import androidx.room.* 4 | import com.xinto.mauth.db.dao.account.entity.EntityAccount 5 | import kotlinx.coroutines.flow.Flow 6 | import java.util.* 7 | 8 | @Dao 9 | interface AccountsDao { 10 | 11 | @Query("SELECT * FROM accounts") 12 | fun observeAll(): Flow> 13 | 14 | @Query("SELECT * FROM accounts") 15 | suspend fun getAll(): List 16 | 17 | @Query("SELECT * FROM accounts WHERE id = :id") 18 | suspend fun getById(id: UUID): EntityAccount? 19 | 20 | @Upsert 21 | suspend fun upsert(entityAccount: EntityAccount) 22 | 23 | @Delete 24 | suspend fun delete(entityAccount: EntityAccount) 25 | 26 | @Query("DELETE FROM accounts WHERE id in (:ids)") 27 | suspend fun delete(ids: Set) 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/dao/account/entity/EntityAccount.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db.dao.account.entity 2 | 3 | import android.net.Uri 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import com.xinto.mauth.core.otp.model.OtpDigest 8 | import com.xinto.mauth.core.otp.model.OtpType 9 | import java.util.UUID 10 | 11 | @Entity(tableName = "accounts") 12 | data class EntityAccount( 13 | @PrimaryKey 14 | @ColumnInfo(name = "id", typeAffinity = ColumnInfo.BLOB) 15 | val id: UUID = UUID.randomUUID(), 16 | 17 | @ColumnInfo(name = "icon") 18 | val icon: Uri?, 19 | 20 | @ColumnInfo(name = "secret") 21 | val secret: String, 22 | 23 | @ColumnInfo(name = "label") 24 | val label: String, 25 | 26 | @ColumnInfo(name = "issuer") 27 | val issuer: String, 28 | 29 | @ColumnInfo(name = "algorithm", defaultValue = "0") 30 | val algorithm: OtpDigest, 31 | 32 | @ColumnInfo(name = "type", defaultValue = "0") 33 | val type: OtpType, 34 | 35 | @ColumnInfo(name = "digits", defaultValue = "6") 36 | val digits: Int, 37 | 38 | @ColumnInfo(name = "period", defaultValue = "30") 39 | val period: Int, 40 | 41 | @ColumnInfo(name = "create_date", defaultValue = "0") 42 | val createDateMillis: Long 43 | ) 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/dao/rtdata/RtdataDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db.dao.rtdata 2 | 3 | import androidx.room.* 4 | import com.xinto.mauth.db.dao.rtdata.entity.EntityCountData 5 | import kotlinx.coroutines.flow.Flow 6 | import java.util.UUID 7 | 8 | @Dao 9 | interface RtdataDao { 10 | 11 | @Query("SELECT * FROM countdata") 12 | fun observeCountData(): Flow> 13 | 14 | @Query("SELECT * FROM countdata WHERE account_id = :accountId") 15 | fun observeAccountCountData(accountId: UUID): Flow 16 | 17 | @Query("SELECT count FROM countdata WHERE account_id = :accountId") 18 | suspend fun getAccountCounter(accountId: UUID): Int 19 | 20 | @Upsert 21 | suspend fun upsertCountData(countData: EntityCountData) 22 | 23 | @Query("UPDATE countdata SET count = count + 1 WHERE account_id = :accountId") 24 | suspend fun incrementAccountCounter(accountId: UUID) 25 | 26 | @Query("UPDATE countdata SET count = :counter WHERE account_id = :accountId") 27 | suspend fun setAccountCounter(accountId: UUID, counter: Int) 28 | 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/db/dao/rtdata/entity/EntityCountData.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.db.dao.rtdata.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import java.util.UUID 7 | 8 | @Entity(tableName = "countdata") 9 | data class EntityCountData( 10 | @PrimaryKey 11 | @ColumnInfo(name = "account_id") 12 | val accountId: UUID, 13 | 14 | @ColumnInfo(name = "count") 15 | val count: Int, 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/domain/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.domain 2 | 3 | import com.xinto.mauth.core.auth.AuthManager 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.first 6 | import kotlinx.coroutines.flow.map 7 | 8 | class AuthRepository( 9 | private val authManager: AuthManager 10 | ) { 11 | 12 | private val liveCode = authManager.getCode() 13 | 14 | fun observeIsProtected(): Flow { 15 | return liveCode.map { it != null } 16 | } 17 | 18 | suspend fun isProtected(): Boolean { 19 | return liveCode.first() != null 20 | } 21 | 22 | suspend fun validate(code: String): Boolean { 23 | return liveCode.first() == code 24 | } 25 | 26 | fun updateCode(code: String) { 27 | authManager.setCode(code) 28 | } 29 | 30 | fun removeCode() { 31 | authManager.removeCode() 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/domain/QrRepository.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.domain 2 | 3 | import android.graphics.Bitmap 4 | import com.xinto.mauth.core.camera.ZxingDecoder 5 | 6 | class QrRepository { 7 | 8 | fun decodeQrImage(image: Bitmap): String? { 9 | val pixels = IntArray(image.width * image.height) 10 | image.getPixels(pixels, 0, image.width, 0, 0, image.width, image.height) 11 | return ZxingDecoder.decodeRgbLuminanceSource( 12 | pixels = pixels, 13 | width = image.width, 14 | height = image.height, 15 | onSuccess = { 16 | return it.text 17 | }, 18 | onError = { 19 | null 20 | } 21 | ) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/domain/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.domain 2 | 3 | import com.xinto.mauth.core.settings.Settings 4 | 5 | class SettingsRepository(private val settings: Settings) : Settings by settings -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccount.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.domain.account.model 2 | 3 | import android.net.Uri 4 | import androidx.compose.runtime.Immutable 5 | import com.xinto.mauth.core.otp.model.OtpDigest 6 | import java.util.UUID 7 | 8 | @Immutable 9 | sealed class DomainAccount { 10 | abstract val id: UUID 11 | abstract val icon: Uri? 12 | abstract val secret: String 13 | abstract val label: String 14 | abstract val issuer: String 15 | abstract val algorithm: OtpDigest 16 | abstract val digits: Int 17 | abstract val createdMillis: Long 18 | 19 | val shortLabel by lazy { 20 | label.filter { 21 | it.isUpperCase() 22 | }.ifEmpty { 23 | label[0].uppercase() 24 | }.take(3) 25 | } 26 | 27 | @Immutable 28 | data class Totp( 29 | override val id: UUID, 30 | override val icon: Uri?, 31 | override val secret: String, 32 | override val label: String, 33 | override val issuer: String, 34 | override val algorithm: OtpDigest, 35 | override val digits: Int, 36 | override val createdMillis: Long, 37 | val period: Int 38 | ) : DomainAccount() 39 | 40 | @Immutable 41 | data class Hotp( 42 | override val id: UUID, 43 | override val icon: Uri?, 44 | override val secret: String, 45 | override val label: String, 46 | override val issuer: String, 47 | override val algorithm: OtpDigest, 48 | override val digits: Int, 49 | override val createdMillis: Long, 50 | ) : DomainAccount() 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/domain/account/model/DomainAccountInfo.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.domain.account.model 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import androidx.compose.runtime.Immutable 6 | import com.xinto.mauth.core.otp.model.OtpDigest 7 | import com.xinto.mauth.core.otp.model.OtpType 8 | import kotlinx.parcelize.Parcelize 9 | import java.util.UUID 10 | 11 | @Immutable 12 | @Parcelize 13 | data class DomainAccountInfo( 14 | val id: UUID, 15 | val icon: Uri?, 16 | val label: String, 17 | val issuer: String, 18 | val secret: String, 19 | val algorithm: OtpDigest, 20 | val type: OtpType, 21 | val digits: Int, 22 | val counter: Int, 23 | val period: Int, 24 | val createdMillis: Long 25 | ) : Parcelable { 26 | 27 | companion object { 28 | fun new(): DomainAccountInfo { 29 | return DomainAccountInfo( 30 | id = UUID.randomUUID(), 31 | icon = null, 32 | label = "", 33 | issuer = "", 34 | secret = "", 35 | algorithm = OtpDigest.SHA1, 36 | type = OtpType.TOTP, 37 | digits = 6, 38 | counter = 0, 39 | period = 30, 40 | createdMillis = System.currentTimeMillis() 41 | ) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/domain/account/model/DomainExportAccount.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.domain.account.model 2 | 3 | import android.net.Uri 4 | import java.util.UUID 5 | 6 | data class DomainExportAccount( 7 | val id: UUID, 8 | val icon: Uri?, 9 | val label: String, 10 | val issuer: String, 11 | val url: String 12 | ) { 13 | val shortLabel = label.take(1) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/domain/otp/model/DomainOtpRealtimeData.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.domain.otp.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | sealed interface DomainOtpRealtimeData { 7 | val code: String 8 | 9 | @Immutable 10 | data class Totp( 11 | override val code: String, 12 | val progress: Float, 13 | val countdown: Int, 14 | ) : DomainOtpRealtimeData 15 | 16 | @Immutable 17 | data class Hotp( 18 | override val code: String, 19 | val count: Int, 20 | ) : DomainOtpRealtimeData 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/TwoPaneCard.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.combinedClickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.ElevatedCard 8 | import androidx.compose.material3.HorizontalDivider 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun TwoPaneCard( 17 | selected: Boolean, 18 | modifier: Modifier = Modifier, 19 | expanded: Boolean = true, 20 | topContent: @Composable () -> Unit, 21 | bottomContent: @Composable () -> Unit, 22 | onClick: () -> Unit, 23 | onLongClick: () -> Unit, 24 | ) { 25 | val shape by animateRoundedCornerShapeAsState( 26 | targetValue = if (selected) MaterialTheme.shapes.small else MaterialTheme.shapes.large, 27 | ) 28 | ElevatedCard( 29 | modifier = modifier, 30 | shape = shape, 31 | ) { 32 | Column( 33 | modifier = Modifier 34 | .combinedClickable( 35 | onClick = onClick, 36 | onLongClick = onLongClick, 37 | ) 38 | .padding(12.dp) 39 | ) { 40 | topContent() 41 | AnimatedVisibility( 42 | visible = expanded, 43 | ) { 44 | Column { 45 | HorizontalDivider(Modifier.padding(vertical = 12.dp)) 46 | bottomContent() 47 | } 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/Uri.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.Immutable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.platform.LocalContext 10 | 11 | @Composable 12 | fun rememberUriHandler(): UriHandler { 13 | val context = LocalContext.current 14 | return remember(context) { 15 | UriHandler(context) 16 | } 17 | } 18 | 19 | @Immutable 20 | class UriHandler(private val context: Context) { 21 | 22 | fun openUrl(url: String) { 23 | context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/UriImage.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component 2 | 3 | import android.net.Uri 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.layout.ContentScale 8 | import androidx.compose.ui.platform.LocalContext 9 | import coil.compose.rememberAsyncImagePainter 10 | import coil.request.ImageRequest 11 | 12 | @Composable 13 | fun UriImage( 14 | uri: Uri, 15 | modifier: Modifier = Modifier, 16 | contentDescription: String? = null 17 | ) { 18 | val context = LocalContext.current 19 | Image( 20 | modifier = modifier, 21 | painter = rememberAsyncImagePainter( 22 | model = ImageRequest.Builder(context) 23 | .data(uri) 24 | .build() 25 | ), 26 | contentDescription = contentDescription, 27 | contentScale = ContentScale.Crop 28 | ) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/form/ComboBoxFormField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.form 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.material3.DropdownMenuItem 6 | import androidx.compose.material3.ExposedDropdownMenuBox 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.MenuAnchorType 9 | import androidx.compose.material3.OutlinedTextField 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.rotate 17 | import androidx.compose.ui.res.painterResource 18 | import androidx.compose.ui.res.stringResource 19 | import com.xinto.mauth.R 20 | 21 | class ComboBoxFormField>( 22 | initial: E, 23 | 24 | @StringRes 25 | private val label: Int 26 | ) : FormField(initial, id = label) { 27 | 28 | private val clazz = initial.declaringJavaClass 29 | 30 | @Composable 31 | override fun invoke(modifier: Modifier) { 32 | val (expanded, setExpanded) = remember { 33 | mutableStateOf(false) 34 | } 35 | ExposedDropdownMenuBox( 36 | expanded = expanded, 37 | onExpandedChange = setExpanded 38 | ) { 39 | OutlinedTextField( 40 | modifier = modifier.menuAnchor(MenuAnchorType.PrimaryEditable), 41 | value = value.name, 42 | onValueChange = {}, 43 | singleLine = true, 44 | label = { 45 | Text(stringResource(label)) 46 | }, 47 | readOnly = true, 48 | trailingIcon = { 49 | val iconRotation by animateFloatAsState(if (expanded) 180f else 0f) 50 | Icon( 51 | modifier = Modifier.rotate(iconRotation), 52 | painter = painterResource(R.drawable.ic_keyboard_arrow_down), 53 | contentDescription = null 54 | ) 55 | }, 56 | isError = error 57 | ) 58 | ExposedDropdownMenu( 59 | expanded = expanded, 60 | onDismissRequest = { setExpanded(false) } 61 | ) { 62 | clazz.enumConstants!!.forEach { 63 | DropdownMenuItem( 64 | text = { Text(it.name) }, 65 | onClick = { 66 | setExpanded(false) 67 | value = it 68 | } 69 | ) 70 | } 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/form/Form.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.form 2 | 3 | abstract class Form { 4 | 5 | abstract fun validate(): T? 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/form/FormField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.form 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.compose.ui.Modifier 9 | 10 | @Stable 11 | abstract class FormField( 12 | initial: T, 13 | val id: Int 14 | ) { 15 | 16 | var value by mutableStateOf(initial) 17 | protected set 18 | 19 | var error by mutableStateOf(false) 20 | private set 21 | 22 | @Composable 23 | abstract operator fun invoke(modifier: Modifier = Modifier) 24 | 25 | fun validate(): Boolean { 26 | return isValid().also { 27 | error = !it 28 | } 29 | } 30 | 31 | protected open fun isValid(): Boolean { 32 | return true 33 | } 34 | 35 | override fun equals(other: Any?): Boolean { 36 | if (this === other) return true 37 | if (other !is FormField<*>) return false 38 | 39 | if (id != other.id) return false 40 | 41 | return true 42 | } 43 | 44 | override fun hashCode(): Int { 45 | return id 46 | } 47 | 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/form/IntFormField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.form 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.foundation.text.KeyboardOptions 5 | import androidx.compose.material3.OutlinedTextField 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.compose.ui.text.input.KeyboardType 11 | import com.xinto.mauth.R 12 | 13 | class IntFormField( 14 | initial: Int, 15 | 16 | @StringRes 17 | private val label: Int, 18 | 19 | private val min: Int = Int.MIN_VALUE, 20 | private val max: Int = Int.MAX_VALUE 21 | ) : FormField(initial.toString(), id = label) { 22 | 23 | @Composable 24 | override fun invoke(modifier: Modifier) { 25 | OutlinedTextField( 26 | modifier = modifier, 27 | value = value, 28 | onValueChange = { 29 | value = it 30 | }, 31 | label = { 32 | Text(stringResource(label)) 33 | }, 34 | supportingText = if (max == Int.MAX_VALUE) null else { -> 35 | Text(stringResource(R.string.account_data_status_range, min.toString(), max.toString())) 36 | }, 37 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), 38 | isError = error 39 | ) 40 | } 41 | 42 | override fun isValid(): Boolean { 43 | val intValue = value.toIntOrNull() ?: return false 44 | 45 | return intValue in min..max 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/form/LazyForm.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.form 2 | 3 | import androidx.compose.foundation.lazy.grid.GridItemSpan 4 | import androidx.compose.foundation.lazy.grid.LazyGridScope 5 | 6 | interface LazyGridForm { 7 | 8 | operator fun LazyGridScope.invoke() 9 | 10 | } 11 | 12 | fun LazyGridScope.form(lazyGridForm: LazyGridForm) { 13 | with(lazyGridForm) { 14 | this@form.invoke() 15 | } 16 | } 17 | 18 | inline fun > LazyGridScope.formfield(field: T) { 19 | item( 20 | contentType = { field }, 21 | key = field.id 22 | ) { 23 | field() 24 | } 25 | } 26 | inline fun > LazyGridScope.singleFormfield(field: T) { 27 | item( 28 | contentType = field, 29 | key = field.id, 30 | span = { GridItemSpan(maxCurrentLineSpan) } 31 | ) { 32 | field() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/form/PasswordFormField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.form 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.IconButton 7 | import androidx.compose.material3.OutlinedTextField 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.saveable.rememberSaveable 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.input.PasswordVisualTransformation 19 | import androidx.compose.ui.text.input.VisualTransformation 20 | import com.xinto.mauth.R 21 | 22 | class PasswordFormField( 23 | initial: String, 24 | 25 | @StringRes 26 | private val label: Int, 27 | 28 | @DrawableRes 29 | private val icon: Int, 30 | private val required: Boolean = false 31 | ) : FormField(initial, id = label) { 32 | 33 | @Composable 34 | override fun invoke(modifier: Modifier) { 35 | var showPassword by rememberSaveable { mutableStateOf(false) } 36 | val visualTransformation = remember(showPassword) { 37 | if (showPassword) { 38 | VisualTransformation.None 39 | } else { 40 | PasswordVisualTransformation() 41 | } 42 | } 43 | OutlinedTextField( 44 | modifier = modifier, 45 | value = value, 46 | onValueChange = { 47 | value = it 48 | }, 49 | label = { 50 | Text(stringResource(label)) 51 | }, 52 | leadingIcon = if (icon == 0) null else { -> 53 | Icon( 54 | painter = painterResource(icon), 55 | contentDescription = null 56 | ) 57 | }, 58 | trailingIcon = { 59 | IconButton(onClick = { showPassword = !showPassword }) { 60 | val visible = painterResource(R.drawable.ic_visibility) 61 | val notVisible = painterResource(R.drawable.ic_visibility_off) 62 | Icon( 63 | painter = if (showPassword) visible else notVisible, 64 | contentDescription = null 65 | ) 66 | } 67 | }, 68 | supportingText = if (!required) null else { -> 69 | Text(stringResource(R.string.account_data_status_required)) 70 | }, 71 | visualTransformation = visualTransformation, 72 | isError = error 73 | ) 74 | } 75 | 76 | override fun isValid(): Boolean { 77 | if (!required) return true 78 | 79 | return value.isNotEmpty() 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/form/TextFormField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.form 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.OutlinedTextField 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.res.stringResource 12 | import com.xinto.mauth.R 13 | 14 | class TextFormField( 15 | initial: String, 16 | 17 | @StringRes 18 | private val label: Int, 19 | 20 | @DrawableRes 21 | private val icon: Int = 0, 22 | private val required: Boolean = false 23 | ) : FormField(initial, id = label) { 24 | 25 | @Composable 26 | override fun invoke(modifier: Modifier) { 27 | OutlinedTextField( 28 | modifier = modifier, 29 | value = value, 30 | onValueChange = { 31 | value = it 32 | }, 33 | label = { 34 | Text(stringResource(label)) 35 | }, 36 | leadingIcon = if (icon == 0) null else { -> 37 | Icon( 38 | painter = painterResource(icon), 39 | contentDescription = null 40 | ) 41 | }, 42 | supportingText = if (!required) null else { -> 43 | Text(stringResource(R.string.account_data_status_required)) 44 | }, 45 | isError = error 46 | ) 47 | } 48 | 49 | override fun isValid(): Boolean { 50 | if (!required) return true 51 | 52 | return value.isNotEmpty() 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/lazygroup/GroupedItemType.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.lazygroup 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | enum class GroupedItemType { 6 | First, 7 | Middle, 8 | Last, 9 | Only 10 | } 11 | 12 | val LocalGroupedItemType = compositionLocalOf { GroupedItemType.Only } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/component/pinboard/PinDisplay.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.component.pinboard 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.animation.animateContentSize 6 | import androidx.compose.animation.core.MutableTransitionState 7 | import androidx.compose.animation.scaleIn 8 | import androidx.compose.animation.scaleOut 9 | import androidx.compose.foundation.layout.Arrangement 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalInspectionMode 23 | import androidx.compose.ui.text.style.TextAlign 24 | import androidx.compose.ui.tooling.preview.Preview 25 | import androidx.compose.ui.unit.dp 26 | import com.xinto.mauth.ui.theme.MauthTheme 27 | 28 | @Composable 29 | fun PinDisplay( 30 | length: Int, 31 | modifier: Modifier = Modifier, 32 | error: Boolean = false, 33 | ) { 34 | val inspectionMode = LocalInspectionMode.current 35 | val color = when (error) { 36 | true -> MaterialTheme.colorScheme.errorContainer 37 | false -> MaterialTheme.colorScheme.secondaryContainer 38 | } 39 | Surface( 40 | modifier = modifier, 41 | color = color, 42 | shape = CircleShape 43 | ) { 44 | Row( 45 | modifier = Modifier 46 | .height(64.dp) 47 | .padding(horizontal = 8.dp) 48 | .animateContentSize(), 49 | horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), 50 | verticalAlignment = Alignment.CenterVertically 51 | ) { 52 | for (i in 0.. = emptyList() 36 | ) : MauthDestination() 37 | 38 | @Parcelize 39 | data object PinSetup : MauthDestination() 40 | 41 | @Parcelize 42 | data object PinRemove : MauthDestination() 43 | 44 | @Parcelize 45 | data object Theme : MauthDestination() 46 | 47 | @Parcelize 48 | data object About : MauthDestination() 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/about/AboutLinks.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.about 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import androidx.compose.runtime.Immutable 6 | import com.xinto.mauth.R 7 | 8 | @Immutable 9 | data class AboutLink( 10 | @DrawableRes 11 | val icon: Int, 12 | 13 | @StringRes 14 | val title: Int, 15 | 16 | val url: String 17 | ) { 18 | companion object { 19 | val defaultLinks = setOf( 20 | AboutLink( 21 | icon = R.drawable.ic_github, 22 | title = R.string.about_links_source, 23 | url = "https://github.com/X1nto/Mauth" 24 | ), 25 | AboutLink( 26 | icon = R.drawable.ic_bug, 27 | title = R.string.about_links_feedback, 28 | url = "https://github.com/X1nto/Mauth/issues" 29 | ), 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/AccountScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | sealed interface AccountScreenState { 7 | 8 | @Immutable 9 | data object Loading : AccountScreenState 10 | 11 | @Immutable 12 | data class Success(val form: AccountForm) : AccountScreenState 13 | 14 | @Immutable 15 | data class Error(val error: String) : AccountScreenState 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/AccountViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account 2 | 3 | import android.app.Application 4 | import androidx.lifecycle.AndroidViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.xinto.mauth.domain.account.AccountRepository 7 | import com.xinto.mauth.domain.account.model.DomainAccountInfo 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.flow.catch 11 | import kotlinx.coroutines.flow.launchIn 12 | import kotlinx.coroutines.flow.onEach 13 | import kotlinx.coroutines.launch 14 | import java.util.UUID 15 | 16 | sealed interface AccountViewModelParams { 17 | @JvmInline 18 | value class Id(val id: UUID) : AccountViewModelParams 19 | 20 | @JvmInline 21 | value class Prefilled(val accountInfo: DomainAccountInfo) : AccountViewModelParams 22 | } 23 | 24 | class AccountViewModel( 25 | application: Application, 26 | 27 | params: AccountViewModelParams, 28 | private val accounts: AccountRepository 29 | ) : AndroidViewModel(application) { 30 | 31 | private val _initialInfo = MutableStateFlow(null) 32 | 33 | private val _state = MutableStateFlow(AccountScreenState.Loading) 34 | val state = _state.asStateFlow() 35 | 36 | init { 37 | when (params) { 38 | is AccountViewModelParams.Id -> { 39 | accounts.getAccountInfo(params.id) 40 | .onEach { 41 | _initialInfo.value = it 42 | _state.value = AccountScreenState.Success(AccountForm(it)) 43 | }.catch { 44 | _state.value = AccountScreenState.Error(it.localizedMessage ?: it.message ?: it.stackTraceToString()) 45 | }.launchIn(viewModelScope) 46 | } 47 | is AccountViewModelParams.Prefilled -> { 48 | _initialInfo.value = params.accountInfo 49 | _state.value = AccountScreenState.Success(AccountForm(params.accountInfo)) 50 | } 51 | } 52 | } 53 | 54 | fun saveData(accountInfo: DomainAccountInfo) { 55 | viewModelScope.launch { 56 | accounts.putAccount(accountInfo) 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountComboBox.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account.component 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material3.DropdownMenuItem 6 | import androidx.compose.material3.ExposedDropdownMenuBox 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.OutlinedTextField 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.rotate 16 | import androidx.compose.ui.res.painterResource 17 | import com.xinto.mauth.R 18 | import kotlin.enums.EnumEntries 19 | 20 | @Composable 21 | fun > AccountComboBox( 22 | values: EnumEntries, 23 | value: E, 24 | onValueChange: (E) -> Unit, 25 | label: (@Composable () -> Unit)? = null 26 | ) { 27 | val (expanded, setExpanded) = remember { 28 | mutableStateOf(false) 29 | } 30 | ExposedDropdownMenuBox( 31 | expanded = expanded, 32 | onExpandedChange = setExpanded 33 | ) { 34 | OutlinedTextField( 35 | modifier = Modifier.fillMaxWidth().menuAnchor(), 36 | value = value.name, 37 | onValueChange = {}, 38 | singleLine = true, 39 | label = label, 40 | readOnly = true, 41 | trailingIcon = { 42 | val iconRotation by animateFloatAsState(if (expanded) 180f else 0f) 43 | Icon( 44 | modifier = Modifier.rotate(iconRotation), 45 | painter = painterResource(R.drawable.ic_keyboard_arrow_down), 46 | contentDescription = null 47 | ) 48 | } 49 | ) 50 | ExposedDropdownMenu( 51 | expanded = expanded, 52 | onDismissRequest = { setExpanded(false) } 53 | ) { 54 | values.forEach { 55 | DropdownMenuItem( 56 | text = { Text(it.name) }, 57 | onClick = { 58 | setExpanded(false) 59 | onValueChange(it) 60 | } 61 | ) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountDataField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account.component 2 | 3 | import androidx.compose.foundation.text.KeyboardOptions 4 | import androidx.compose.material3.OutlinedTextField 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import androidx.compose.ui.text.input.VisualTransformation 9 | import com.xinto.mauth.R 10 | 11 | @Composable 12 | fun AccountDataField( 13 | value: String, 14 | onValueChange: (String) -> Unit, 15 | required: Boolean = false, 16 | label: (@Composable () -> Unit)? = null, 17 | leadingIcon: (@Composable () -> Unit)? = null, 18 | trailingIcon: (@Composable () -> Unit)? = null, 19 | visualTransformation: VisualTransformation = VisualTransformation.None, 20 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 21 | ) { 22 | OutlinedTextField( 23 | value = value, 24 | onValueChange = onValueChange, 25 | singleLine = true, 26 | label = label, 27 | leadingIcon = leadingIcon, 28 | trailingIcon = trailingIcon, 29 | supportingText = if (required) { -> 30 | Text(stringResource(R.string.account_data_status_required)) 31 | } else null, 32 | visualTransformation = visualTransformation, 33 | keyboardOptions = keyboardOptions, 34 | ) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountExitDialog.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account.component 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import com.xinto.mauth.R 9 | 10 | @Composable 11 | fun AccountExitDialog( 12 | onCancel: () -> Unit, 13 | onConfirm: () -> Unit, 14 | ) { 15 | AlertDialog( 16 | onDismissRequest = onCancel, 17 | title = { 18 | Text(stringResource(R.string.account_discard_title)) 19 | }, 20 | text = { 21 | Text(stringResource(R.string.account_discard_subtitle)) 22 | }, 23 | confirmButton = { 24 | TextButton(onClick = onConfirm) { 25 | Text(stringResource(R.string.account_discard_buttons_discard)) 26 | } 27 | }, 28 | dismissButton = { 29 | TextButton(onClick = onCancel) { 30 | Text(stringResource(R.string.account_discard_buttons_cancel)) 31 | } 32 | } 33 | ) 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/component/AccountNumberField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account.component 2 | 3 | import androidx.compose.foundation.text.KeyboardOptions 4 | import androidx.compose.material3.OutlinedTextField 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.text.input.KeyboardType 8 | 9 | @Composable 10 | fun AccountNumberField( 11 | value: String, 12 | onValueChange: (String) -> Unit, 13 | label: (@Composable () -> Unit)? = null, 14 | supportingText: (@Composable () -> Unit)? = null, 15 | min: Int = 0, 16 | max: Int = Int.MAX_VALUE 17 | ) { 18 | OutlinedTextField( 19 | value = value, 20 | onValueChange = onValueChange, 21 | singleLine = true, 22 | keyboardOptions = remember { 23 | KeyboardOptions(keyboardType = KeyboardType.Number) 24 | }, 25 | supportingText = supportingText, 26 | label = label, 27 | isError = value.toIntOrNull() == null || (value.toInt() < min || value.toInt() > max) 28 | ) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenError.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import com.xinto.mauth.R 15 | 16 | @Composable 17 | fun AccountScreenError() { 18 | Column( 19 | modifier = Modifier.fillMaxSize(), 20 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), 21 | horizontalAlignment = Alignment.CenterHorizontally 22 | ) { 23 | Icon( 24 | painter = painterResource(R.drawable.ic_error), 25 | contentDescription = null 26 | ) 27 | Text(stringResource(R.string.account_error)) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenLoading.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account.state 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun AccountScreenLoading() { 12 | Box( 13 | modifier = Modifier.fillMaxSize(), 14 | contentAlignment = Alignment.Center 15 | ) { 16 | CircularProgressIndicator() 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/account/state/AccountScreenSuccess.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.account.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.lazy.grid.GridCells 7 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import com.xinto.mauth.ui.component.form.form 12 | import com.xinto.mauth.ui.screen.account.AccountForm 13 | 14 | @Composable 15 | fun AccountScreenSuccess(form: AccountForm) { 16 | LazyVerticalGrid( 17 | modifier = Modifier.fillMaxSize(), 18 | verticalArrangement = Arrangement.spacedBy(8.dp), 19 | horizontalArrangement = Arrangement.spacedBy(8.dp), 20 | contentPadding = PaddingValues(16.dp), 21 | columns = GridCells.Fixed(2) 22 | ) { 23 | form(form) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/auth/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.auth 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.xinto.mauth.domain.AuthRepository 6 | import com.xinto.mauth.domain.SettingsRepository 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.SharingStarted 9 | import kotlinx.coroutines.flow.asStateFlow 10 | import kotlinx.coroutines.flow.stateIn 11 | import kotlinx.coroutines.flow.update 12 | 13 | class AuthViewModel( 14 | private val authRepository: AuthRepository, 15 | private val settingsRepository: SettingsRepository 16 | ) : ViewModel() { 17 | 18 | private val _code = MutableStateFlow("") 19 | val code = _code.asStateFlow() 20 | 21 | val useBiometrics = settingsRepository.getUseBiometrics() 22 | .stateIn( 23 | scope = viewModelScope, 24 | started = SharingStarted.WhileSubscribed(5000), 25 | initialValue = false 26 | ) 27 | 28 | fun insertNumber(number: Char) { 29 | _code.update { it + number } 30 | } 31 | 32 | fun deleteNumber() { 33 | _code.update { it.dropLast(1) } 34 | } 35 | 36 | fun clear() { 37 | _code.value = "" 38 | } 39 | 40 | suspend fun validate(code: String): Boolean { 41 | return authRepository.validate(code) 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/export/ExportScreen.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.export 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.LargeTopAppBar 10 | import androidx.compose.material3.Scaffold 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 19 | import com.xinto.mauth.R 20 | import com.xinto.mauth.domain.account.model.DomainExportAccount 21 | import com.xinto.mauth.ui.screen.export.state.ExportScreenError 22 | import com.xinto.mauth.ui.screen.export.state.ExportScreenLoading 23 | import com.xinto.mauth.ui.screen.export.state.ExportScreenSuccess 24 | import org.koin.androidx.compose.koinViewModel 25 | import org.koin.core.parameter.parametersOf 26 | import java.util.UUID 27 | 28 | @Composable 29 | fun ExportScreen( 30 | onBackNavigate: () -> Unit, 31 | accounts: List 32 | ) { 33 | BackHandler(onBack = onBackNavigate) 34 | val viewModel: ExportViewModel = koinViewModel { 35 | parametersOf(accounts) 36 | } 37 | val state by viewModel.state.collectAsStateWithLifecycle() 38 | ExportScreen( 39 | onBackNavigate = onBackNavigate, 40 | onCopyUrlToClipboard = { 41 | viewModel.copyUrlToClipboard( 42 | label = it.label, 43 | url = it.url 44 | ) 45 | }, 46 | state = state 47 | ) 48 | } 49 | 50 | @Composable 51 | fun ExportScreen( 52 | onBackNavigate: () -> Unit, 53 | onCopyUrlToClipboard: (DomainExportAccount) -> Unit, 54 | state: ExportScreenState 55 | ) { 56 | Scaffold( 57 | topBar = { 58 | LargeTopAppBar( 59 | title = { 60 | Text(text = stringResource(R.string.export_title)) 61 | }, 62 | navigationIcon = { 63 | IconButton(onClick = onBackNavigate) { 64 | Icon( 65 | painter = painterResource(id = R.drawable.ic_arrow_back), 66 | contentDescription = null 67 | ) 68 | } 69 | } 70 | ) 71 | } 72 | ) { paddingValues -> 73 | Box( 74 | modifier = Modifier 75 | .fillMaxSize() 76 | .padding(paddingValues), 77 | contentAlignment = Alignment.Center 78 | ) { 79 | when (state) { 80 | is ExportScreenState.Loading -> { 81 | ExportScreenLoading() 82 | } 83 | is ExportScreenState.Success -> { 84 | ExportScreenSuccess( 85 | accounts = state.accounts, 86 | onCopyUrlToClipboard = onCopyUrlToClipboard 87 | ) 88 | } 89 | is ExportScreenState.Error -> { 90 | ExportScreenError() 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/export/ExportScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.export 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.mauth.domain.account.model.DomainExportAccount 5 | 6 | @Immutable 7 | sealed interface ExportScreenState { 8 | 9 | @Immutable 10 | data object Loading : ExportScreenState 11 | 12 | @Immutable 13 | data class Success(val accounts: List) : ExportScreenState 14 | 15 | @Immutable 16 | data object Error : ExportScreenState 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/export/ExportViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.export 2 | 3 | import android.app.Application 4 | import android.content.ClipData 5 | import android.content.ClipboardManager 6 | import android.os.Build 7 | import android.os.PersistableBundle 8 | import android.widget.Toast 9 | import androidx.core.content.getSystemService 10 | import androidx.lifecycle.AndroidViewModel 11 | import androidx.lifecycle.viewModelScope 12 | import com.xinto.mauth.Mauth 13 | import com.xinto.mauth.R 14 | import com.xinto.mauth.domain.account.AccountRepository 15 | import com.xinto.mauth.util.catchMap 16 | import kotlinx.coroutines.flow.SharingStarted 17 | import kotlinx.coroutines.flow.map 18 | import kotlinx.coroutines.flow.stateIn 19 | import java.util.UUID 20 | 21 | class ExportViewModel( 22 | application: Application, 23 | 24 | private val accounts: List, 25 | private val accountRepository: AccountRepository, 26 | ) : AndroidViewModel(application) { 27 | 28 | val state = accountRepository.getAccounts() 29 | .map { accounts -> 30 | val filteredAccounts = if (this.accounts.isEmpty()) { 31 | accounts 32 | } else { 33 | accounts.filter { this.accounts.contains(it.id) } 34 | } 35 | 36 | val exportAccounts = filteredAccounts.map { 37 | with (accountRepository) { 38 | it.toExportAccount() 39 | } 40 | } 41 | ExportScreenState.Success(exportAccounts) 42 | }.catchMap { 43 | ExportScreenState.Error 44 | }.stateIn( 45 | scope = viewModelScope, 46 | started = SharingStarted.WhileSubscribed(5000), 47 | initialValue = ExportScreenState.Loading 48 | ) 49 | 50 | fun copyUrlToClipboard(label: String, url: String) { 51 | val application = getApplication() 52 | val clipboardService = application.getSystemService() ?: return 53 | val clipData = ClipData.newPlainText(label, url).apply { 54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 55 | description.extras = PersistableBundle().apply { 56 | putBoolean("android.content.extra.IS_SENSITIVE", true) 57 | } 58 | } 59 | } 60 | clipboardService.setPrimaryClip(clipData) 61 | Toast.makeText(application, R.string.export_url_copy_success, Toast.LENGTH_LONG).show() 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/export/component/ZxingQrImage.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.export.component 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.CircularProgressIndicator 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.ImageBitmap 18 | import androidx.compose.ui.graphics.asImageBitmap 19 | import androidx.compose.ui.graphics.toArgb 20 | import androidx.compose.ui.layout.ContentScale 21 | import com.xinto.mauth.core.camera.ZxingEncoder 22 | 23 | @Composable 24 | fun ZxingQrImage( 25 | data: String, 26 | modifier: Modifier = Modifier, 27 | backgroundColor: Color = Color.White, 28 | contentColor: Color = Color.Black, 29 | contentScale: ContentScale = ContentScale.Fit 30 | ) { 31 | var bitmap by remember { mutableStateOf(null) } 32 | LaunchedEffect(data) { 33 | bitmap = null 34 | bitmap = ZxingEncoder.encodeToBitmap( 35 | data = data, 36 | size = 300, 37 | backgroundColor = backgroundColor.toArgb(), 38 | dataColor = contentColor.toArgb() 39 | ).asImageBitmap() 40 | } 41 | Box(modifier = modifier) { 42 | if (bitmap != null) { 43 | Image( 44 | modifier = Modifier 45 | .aspectRatio(1f) 46 | .fillMaxSize(), 47 | bitmap = bitmap!!, 48 | contentDescription = null, 49 | contentScale = contentScale 50 | ) 51 | } else { 52 | Box( 53 | modifier = Modifier 54 | .aspectRatio(1f) 55 | .fillMaxSize(), 56 | contentAlignment = Alignment.Center 57 | ) { 58 | CircularProgressIndicator() 59 | } 60 | } 61 | } 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/export/state/ExportScreenError.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.export.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.unit.dp 12 | import com.xinto.mauth.R 13 | 14 | @Composable 15 | fun ExportScreenError() { 16 | Column( 17 | modifier = Modifier.fillMaxSize(), 18 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), 19 | horizontalAlignment = Alignment.CenterHorizontally 20 | ) { 21 | Icon( 22 | painter = painterResource(R.drawable.ic_error), 23 | contentDescription = null 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/export/state/ExportScreenLoading.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.export.state 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun ExportScreenLoading() { 12 | Box( 13 | modifier = Modifier.fillMaxSize(), 14 | contentAlignment = Alignment.Center 15 | ) { 16 | CircularProgressIndicator() 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/HomeAddAccountMenu.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import com.xinto.mauth.R 6 | 7 | enum class HomeAddAccountMenu( 8 | @DrawableRes val icon: Int, 9 | @StringRes val title: Int 10 | ) { 11 | ScanQR(R.drawable.ic_qr_code_scanner, R.string.home_addaccount_data_scanqr), 12 | ImageQR(R.drawable.ic_qr_code_2, R.string.home_addaccount_data_imageqr), 13 | Manual(R.drawable.ic_password, R.string.home_addaccount_data_manual) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/HomeMoreMenu.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | import com.xinto.mauth.R 6 | 7 | enum class HomeMoreMenu( 8 | @DrawableRes val icon: Int, 9 | @StringRes val title: Int 10 | ) { 11 | Settings(R.drawable.ic_settings, R.string.settings_title), 12 | Export(R.drawable.ic_export, R.string.export_title), 13 | About(R.drawable.ic_info, R.string.about_title) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/HomeScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.mauth.domain.account.model.DomainAccount 5 | import kotlinx.collections.immutable.ImmutableList 6 | 7 | @Immutable 8 | sealed interface HomeScreenState { 9 | 10 | @Immutable 11 | data object Loading : HomeScreenState 12 | 13 | @Immutable 14 | data object Empty : HomeScreenState 15 | 16 | @Immutable 17 | @JvmInline 18 | value class Success(val accounts: ImmutableList) : HomeScreenState 19 | 20 | @Immutable 21 | @JvmInline 22 | value class Error(val error: String) : HomeScreenState 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/component/HomeDeleteAccountsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home.component 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.FilledTonalButton 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.Text 7 | import androidx.compose.material3.TextButton 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.res.painterResource 10 | import androidx.compose.ui.res.stringResource 11 | import com.xinto.mauth.R 12 | 13 | @Composable 14 | fun HomeDeleteAccountsDialog( 15 | onConfirm: () -> Unit, 16 | onCancel: () -> Unit, 17 | ) { 18 | AlertDialog( 19 | onDismissRequest = onCancel, 20 | icon = { 21 | Icon( 22 | painter = painterResource(R.drawable.ic_delete_forever), 23 | contentDescription = null 24 | ) 25 | }, 26 | title = { 27 | Text(stringResource(R.string.home_delete_title)) 28 | }, 29 | text = { 30 | Text(stringResource(R.string.home_delete_subtitle)) 31 | }, 32 | confirmButton = { 33 | FilledTonalButton(onClick = onConfirm) { 34 | Text(stringResource(R.string.home_delete_button_delete)) 35 | } 36 | }, 37 | dismissButton = { 38 | TextButton(onClick = onCancel) { 39 | Text(stringResource(R.string.home_delete_button_cancel)) 40 | } 41 | } 42 | ) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenEmpty.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.ProvideTextStyle 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.painterResource 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.unit.dp 16 | import com.xinto.mauth.R 17 | 18 | @Composable 19 | fun HomeScreenEmpty(modifier: Modifier = Modifier) { 20 | Column( 21 | modifier = modifier, 22 | verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), 23 | horizontalAlignment = Alignment.CenterHorizontally 24 | ) { 25 | Icon( 26 | modifier = Modifier.size(72.dp), 27 | painter = painterResource(R.drawable.ic_empty_dashboard), 28 | contentDescription = null 29 | ) 30 | ProvideTextStyle(MaterialTheme.typography.headlineSmall) { 31 | Text(stringResource(R.string.home_dashboard_empty)) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenError.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.unit.dp 13 | import com.xinto.mauth.R 14 | 15 | @Composable 16 | fun HomeScreenError(modifier: Modifier = Modifier) { 17 | Column( 18 | modifier = modifier, 19 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), 20 | horizontalAlignment = Alignment.CenterHorizontally 21 | ) { 22 | Icon( 23 | painter = painterResource(R.drawable.ic_error), 24 | contentDescription = null 25 | ) 26 | Text(stringResource(R.string.home_dashboard_error)) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenLoading.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home.state 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material3.CircularProgressIndicator 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | fun HomeScreenLoading(modifier: Modifier = Modifier) { 11 | Box( 12 | modifier = modifier, 13 | contentAlignment = Alignment.Center 14 | ) { 15 | CircularProgressIndicator() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/home/state/HomeScreenSuccess.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.home.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.lazy.grid.GridCells 6 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 7 | import androidx.compose.foundation.lazy.grid.items 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.snapshots.SnapshotStateList 10 | import androidx.compose.runtime.snapshots.SnapshotStateMap 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import com.xinto.mauth.domain.account.model.DomainAccount 14 | import com.xinto.mauth.domain.otp.model.DomainOtpRealtimeData 15 | import com.xinto.mauth.ui.screen.home.component.HomeAccountCard 16 | import kotlinx.collections.immutable.ImmutableList 17 | import java.util.UUID 18 | 19 | @Composable 20 | fun HomeScreenSuccess( 21 | modifier: Modifier = Modifier, 22 | onAccountSelect: (UUID) -> Unit, 23 | onAccountEdit: (UUID) -> Unit, 24 | onAccountCounterIncrease: (UUID) -> Unit, 25 | onAccountCopyCode: (String, String, Boolean) -> Unit, 26 | accounts: ImmutableList, 27 | selectedAccounts: SnapshotStateList, 28 | accountRealtimeData: SnapshotStateMap, 29 | ) { 30 | LazyVerticalGrid( 31 | modifier = modifier, 32 | contentPadding = PaddingValues(16.dp), 33 | verticalArrangement = Arrangement.spacedBy(12.dp), 34 | horizontalArrangement = Arrangement.spacedBy(12.dp), 35 | columns = GridCells.Adaptive(minSize = 250.dp), 36 | ) { 37 | items( 38 | items = accounts, 39 | key = { it.id } 40 | ) { account -> 41 | val realtimeData = accountRealtimeData[account.id] 42 | if (realtimeData != null) { 43 | HomeAccountCard( 44 | onClick = { 45 | if (selectedAccounts.isNotEmpty()) { 46 | onAccountSelect(account.id) 47 | } 48 | }, 49 | onLongClick = { 50 | onAccountSelect(account.id) 51 | }, 52 | onEdit = { 53 | onAccountEdit(account.id) 54 | }, 55 | onCounterClick = { 56 | onAccountCounterIncrease(account.id) 57 | }, 58 | onCopyCode = { 59 | onAccountCopyCode(account.label, realtimeData.code, it) 60 | }, 61 | account = account, 62 | realtimeData = realtimeData, 63 | selected = selectedAccounts.contains(account.id) 64 | ) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/pinremove/PinRemoveScreen.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.pinremove 2 | 3 | import android.content.res.Configuration 4 | import androidx.activity.compose.BackHandler 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.IconButton 7 | import androidx.compose.material3.LargeTopAppBar 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TopAppBar 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.platform.LocalConfiguration 13 | import androidx.compose.ui.res.painterResource 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 16 | import com.xinto.mauth.R 17 | import com.xinto.mauth.ui.component.pinboard.PinScaffold 18 | import com.xinto.mauth.ui.component.pinboard.rememberPinBoardState 19 | import org.koin.androidx.compose.getViewModel 20 | 21 | @Composable 22 | fun PinRemoveScreen( 23 | onExit: () -> Unit 24 | ) { 25 | val viewModel: PinRemoveViewModel = getViewModel() 26 | val state by viewModel.state.collectAsStateWithLifecycle() 27 | BackHandler(onBack = onExit) 28 | PinRemoveScreen( 29 | state = state, 30 | onEnter = { 31 | if (viewModel.removePin()) { 32 | onExit() 33 | } 34 | }, 35 | onBack = onExit, 36 | onNumberEnter = viewModel::addNumber, 37 | onNumberDelete = viewModel::deleteLast, 38 | onAllDelete = viewModel::clear 39 | ) 40 | } 41 | 42 | @Composable 43 | fun PinRemoveScreen( 44 | state: PinRemoveScreenState, 45 | onEnter: () -> Unit, 46 | onBack: () -> Unit, 47 | onNumberEnter: (Char) -> Unit, 48 | onNumberDelete: () -> Unit, 49 | onAllDelete: () -> Unit, 50 | ) { 51 | val orientation = LocalConfiguration.current.orientation 52 | PinScaffold( 53 | codeLength = state.code.length, 54 | error = state is PinRemoveScreenState.Error, 55 | topBar = { 56 | if (orientation == Configuration.ORIENTATION_LANDSCAPE) { 57 | TopAppBar( 58 | title = { AppBarTitle() }, 59 | navigationIcon = { AppBarNavigationIcon(onBack) } 60 | ) 61 | } else { 62 | LargeTopAppBar( 63 | title = { AppBarTitle() }, 64 | navigationIcon = { AppBarNavigationIcon(onBack) } 65 | ) 66 | } 67 | }, 68 | description = null, 69 | useSmallButtons = orientation == Configuration.ORIENTATION_LANDSCAPE, 70 | state = rememberPinBoardState( 71 | showEnter = true, 72 | onNumberClick = onNumberEnter, 73 | onBackspaceClick = onNumberDelete, 74 | onEnterClick = onEnter, 75 | onBackspaceLongClick = onAllDelete 76 | ) 77 | ) 78 | } 79 | 80 | @Composable 81 | fun AppBarTitle() { 82 | Text(stringResource(R.string.pinremove_title)) 83 | } 84 | 85 | @Composable 86 | fun AppBarNavigationIcon(onBack: () -> Unit) { 87 | IconButton(onClick = onBack) { 88 | Icon( 89 | painter = painterResource(R.drawable.ic_arrow_back), 90 | contentDescription = null 91 | ) 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/pinremove/PinRemoveScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.pinremove 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | sealed interface PinRemoveScreenState { 7 | val code: String 8 | 9 | @Immutable 10 | @JvmInline 11 | value class Stale(override val code: String) : PinRemoveScreenState 12 | 13 | @Immutable 14 | data object Error : PinRemoveScreenState { 15 | override val code: String = "" 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/pinremove/PinRemoveViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.pinremove 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.xinto.mauth.domain.AuthRepository 5 | import com.xinto.mauth.domain.SettingsRepository 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | import kotlinx.coroutines.flow.update 9 | import kotlinx.coroutines.runBlocking 10 | 11 | class PinRemoveViewModel( 12 | private val authRepository: AuthRepository, 13 | private val settingsRepository: SettingsRepository 14 | ) : ViewModel() { 15 | 16 | private val _state = MutableStateFlow(PinRemoveScreenState.Stale("")) 17 | val state = _state.asStateFlow() 18 | 19 | /** 20 | * @return true if the screen should exit 21 | */ 22 | fun removePin(): Boolean { 23 | return state.value.let { 24 | runBlocking { 25 | authRepository.validate(it.code).also { valid -> 26 | if (valid) { 27 | authRepository.removeCode() 28 | settingsRepository.setUseBiometrics(false) 29 | } 30 | } 31 | }.also { valid -> 32 | if (!valid) { 33 | _state.value = PinRemoveScreenState.Error 34 | } 35 | } 36 | } 37 | } 38 | 39 | fun addNumber(number: Char) { 40 | _state.update { 41 | when (it) { 42 | is PinRemoveScreenState.Stale -> PinRemoveScreenState.Stale(it.code + number) 43 | is PinRemoveScreenState.Error -> PinRemoveScreenState.Stale(number.toString()) 44 | } 45 | } 46 | } 47 | 48 | fun deleteLast() { 49 | _state.update { 50 | if (it is PinRemoveScreenState.Stale) { 51 | PinRemoveScreenState.Stale(it.code.dropLast(1)) 52 | } else it 53 | } 54 | } 55 | 56 | fun clear() { 57 | _state.value = PinRemoveScreenState.Stale("") 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/pinsetup/PinSetupScreenState.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.pinsetup 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | sealed interface PinSetupScreenState { 7 | @Immutable 8 | data object Initial : PinSetupScreenState 9 | 10 | @Immutable 11 | data object Confirm : PinSetupScreenState 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/pinsetup/PinSetupViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.pinsetup 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.xinto.mauth.domain.AuthRepository 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import kotlinx.coroutines.flow.update 8 | 9 | class PinSetupViewModel( 10 | private val authRepository: AuthRepository 11 | ) : ViewModel() { 12 | 13 | private var initialCode: String? = null 14 | 15 | private val _error = MutableStateFlow(false) 16 | val error = _error.asStateFlow() 17 | 18 | private val _code = MutableStateFlow("") 19 | val code = _code.asStateFlow() 20 | 21 | private val _state = MutableStateFlow(PinSetupScreenState.Initial) 22 | val state = _state.asStateFlow() 23 | 24 | /** 25 | * @return true if the screen should exit 26 | */ 27 | fun next(): Boolean { 28 | if (_code.value.isEmpty()) { 29 | _error.value = true 30 | return false 31 | } 32 | 33 | if (state.value is PinSetupScreenState.Confirm) { 34 | val matches = initialCode == code.value 35 | if (matches) { 36 | authRepository.updateCode(code.value) 37 | } else { 38 | _error.value = true 39 | clear() 40 | } 41 | return matches 42 | } 43 | 44 | _state.value = PinSetupScreenState.Confirm 45 | _code.update { 46 | initialCode = it 47 | "" 48 | } 49 | return false 50 | } 51 | 52 | /** 53 | * @return true if the screen should exit 54 | */ 55 | fun previous(): Boolean { 56 | if (state.value is PinSetupScreenState.Initial) { 57 | return true 58 | } 59 | 60 | clear() 61 | _state.value = PinSetupScreenState.Initial 62 | return false 63 | } 64 | 65 | fun addNumber(number: Char) { 66 | _error.value = false 67 | _code.update { it + number } 68 | } 69 | 70 | fun deleteLast() { 71 | _code.update { it.dropLast(1) } 72 | } 73 | 74 | fun clear() { 75 | _code.value = "" 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/qrscan/QrScanViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.qrscan 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.xinto.mauth.domain.account.model.DomainAccountInfo 5 | import com.xinto.mauth.domain.otp.OtpRepository 6 | 7 | class QrScanViewModel( 8 | private val repository: OtpRepository 9 | ) : ViewModel() { 10 | 11 | fun parseResult(result: com.google.zxing.Result): DomainAccountInfo? { 12 | return repository.parseUriToAccountInfo(result.text) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/qrscan/component/QrScanCamera.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.qrscan.component 2 | 3 | import android.content.Context 4 | import android.view.ViewGroup 5 | import androidx.camera.core.CameraSelector 6 | import androidx.camera.core.ImageAnalysis 7 | import androidx.camera.core.Preview 8 | import androidx.camera.lifecycle.ProcessCameraProvider 9 | import androidx.camera.view.PreviewView 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.DisposableEffect 13 | import androidx.compose.runtime.Stable 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.platform.LocalLifecycleOwner 18 | import androidx.compose.ui.viewinterop.AndroidView 19 | 20 | @Stable 21 | data class CameraState( 22 | val cameraProvider: ProcessCameraProvider, 23 | val analysis: ImageAnalysis, 24 | val preview: Preview, 25 | val cameraSelector: CameraSelector 26 | ) 27 | 28 | @Composable 29 | fun rememberCameraState( 30 | context: Context, 31 | analysis: ImageAnalysis = ImageAnalysis.Builder().build(), 32 | preview: Preview = Preview.Builder().build(), 33 | cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA 34 | ): CameraState { 35 | return remember(context, analysis, preview, cameraSelector) { 36 | CameraState( 37 | cameraProvider = ProcessCameraProvider.getInstance(context).get(), 38 | analysis = analysis, 39 | preview = preview, 40 | cameraSelector = cameraSelector 41 | ) 42 | } 43 | } 44 | 45 | @Composable 46 | fun QrScanCamera( 47 | modifier: Modifier = Modifier, 48 | state: CameraState = rememberCameraState(LocalContext.current), 49 | ) { 50 | val lifecycleOwner = LocalLifecycleOwner.current 51 | DisposableEffect(state, lifecycleOwner) { 52 | state.cameraProvider.unbindAll() 53 | state.cameraProvider.bindToLifecycle( 54 | lifecycleOwner, 55 | state.cameraSelector, 56 | state.preview, 57 | state.analysis 58 | ) 59 | 60 | onDispose { 61 | state.cameraProvider.unbindAll() 62 | } 63 | } 64 | 65 | Box(modifier = modifier) { 66 | AndroidView( 67 | factory = { context -> 68 | PreviewView(context).apply { 69 | scaleType = PreviewView.ScaleType.FILL_CENTER 70 | layoutParams = ViewGroup.LayoutParams( 71 | ViewGroup.LayoutParams.MATCH_PARENT, 72 | ViewGroup.LayoutParams.MATCH_PARENT, 73 | ) 74 | implementationMode = PreviewView.ImplementationMode.COMPATIBLE 75 | } 76 | }, 77 | update = { previewView -> 78 | state.preview.setSurfaceProvider(previewView.surfaceProvider) 79 | } 80 | ) 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/qrscan/component/QrScanPermissionDeniedDialog.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.qrscan.component 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Button 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.res.stringResource 8 | import com.xinto.mauth.R 9 | 10 | @Composable 11 | fun QrScanPermissionDeniedDialog( 12 | shouldShowRationale: Boolean, 13 | onGrantPermission: () -> Unit, 14 | onCancel: () -> Unit, 15 | ) { 16 | AlertDialog( 17 | onDismissRequest = onCancel, 18 | text = { 19 | if (shouldShowRationale) { 20 | Text(stringResource(R.string.qrscan_permissions_subtitle_rationale)) 21 | } else { 22 | Text(stringResource(R.string.qrscan_permissions_subtitle)) 23 | } 24 | }, 25 | confirmButton = { 26 | Button(onClick = onGrantPermission) { 27 | Text(stringResource(R.string.qrscan_permissions_button_grant)) 28 | } 29 | }, 30 | dismissButton = { 31 | Button(onClick = onCancel) { 32 | Text(stringResource(R.string.qrscan_permissions_button_cancel)) 33 | } 34 | } 35 | ) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/qrscan/state/QrScanPermissionDenied.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.qrscan.state 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import com.xinto.mauth.R 15 | 16 | @Composable 17 | fun QrScanPermissionDenied() { 18 | Column( 19 | modifier = Modifier.fillMaxSize(), 20 | verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), 21 | horizontalAlignment = Alignment.CenterHorizontally 22 | ) { 23 | Icon( 24 | painter = painterResource(R.drawable.ic_error), 25 | contentDescription = null 26 | ) 27 | Text(stringResource(R.string.qrscan_error)) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/qrscan/state/QrScanPermissionGranted.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.qrscan.state 2 | 3 | import androidx.camera.core.ImageAnalysis 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.unit.dp 16 | import androidx.core.content.ContextCompat 17 | import com.xinto.mauth.core.camera.QrCodeAnalyzer 18 | import com.xinto.mauth.ui.screen.qrscan.component.QrScanCamera 19 | import com.xinto.mauth.ui.screen.qrscan.component.rememberCameraState 20 | 21 | @Composable 22 | fun QrScanPermissionGranted( 23 | onScan: (com.google.zxing.Result) -> Unit 24 | ) { 25 | Surface( 26 | modifier = Modifier 27 | .padding(32.dp) 28 | .aspectRatio(1f / 1f), 29 | shape = MaterialTheme.shapes.large, 30 | tonalElevation = 1.dp 31 | ) { 32 | Box(contentAlignment = Alignment.Center) { 33 | val context = LocalContext.current 34 | val cameraAnalysis = remember(context) { 35 | ImageAnalysis.Builder() 36 | .build() 37 | .also { analysis -> 38 | analysis.setAnalyzer( 39 | ContextCompat.getMainExecutor(context), 40 | QrCodeAnalyzer( 41 | onSuccess = onScan, 42 | onFail = {} 43 | ) 44 | ) 45 | } 46 | } 47 | QrScanCamera( 48 | modifier = Modifier 49 | .matchParentSize() 50 | .padding(12.dp) 51 | .clip(MaterialTheme.shapes.large), 52 | state = rememberCameraState(context, analysis = cameraAnalysis) 53 | ) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.settings 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.xinto.mauth.domain.AuthRepository 6 | import com.xinto.mauth.domain.SettingsRepository 7 | import kotlinx.coroutines.flow.SharingStarted 8 | import kotlinx.coroutines.flow.stateIn 9 | import kotlinx.coroutines.launch 10 | 11 | class SettingsViewModel( 12 | private val settings: SettingsRepository, 13 | private val authRepository: AuthRepository 14 | ) : ViewModel() { 15 | 16 | val secureMode = settings.getSecureMode() 17 | .stateIn( 18 | scope = viewModelScope, 19 | started = SharingStarted.WhileSubscribed(5000), 20 | initialValue = false 21 | ) 22 | 23 | val pinLock = authRepository.observeIsProtected() 24 | .stateIn( 25 | scope = viewModelScope, 26 | started = SharingStarted.WhileSubscribed(5000), 27 | initialValue = false 28 | ) 29 | 30 | val biometrics = settings.getUseBiometrics() 31 | .stateIn( 32 | scope = viewModelScope, 33 | started = SharingStarted.WhileSubscribed(5000), 34 | initialValue = false 35 | ) 36 | 37 | fun updateSecureMode(newSecureMode: Boolean) { 38 | viewModelScope.launch { 39 | settings.setSecureMode(newSecureMode) 40 | } 41 | } 42 | 43 | fun toggleBiometrics() { 44 | viewModelScope.launch { 45 | settings.setUseBiometrics(!biometrics.value) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/settings/component/SettingsItem.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.settings.component 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.material3.ListItemDefaults 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | import com.xinto.mauth.ui.component.lazygroup.GroupedListItem 10 | 11 | @Composable 12 | fun SettingsItem( 13 | modifier: Modifier = Modifier, 14 | title: @Composable () -> Unit, 15 | description: (@Composable () -> Unit)? = null, 16 | trailing: (@Composable () -> Unit)? = null, 17 | icon: (@Composable () -> Unit)? = null, 18 | enabled: Boolean = true, 19 | ) { 20 | val colors = ListItemDefaults.colors( 21 | headlineColor = MaterialTheme.colorScheme.onSurface.let { 22 | if (!enabled) it.copy(alpha = 0.3f) else it 23 | }, 24 | leadingIconColor = MaterialTheme.colorScheme.onSurface.let { 25 | if (!enabled) it.copy(alpha = 0.38f) else it 26 | }, 27 | trailingIconColor = MaterialTheme.colorScheme.onSurface.let { 28 | if (!enabled) it.copy(alpha = 0.38f) else it 29 | }, 30 | ) 31 | GroupedListItem( 32 | modifier = modifier, 33 | leadingContent = icon, 34 | trailingContent = trailing, 35 | supportingContent = description, 36 | headlineContent = title, 37 | tonalElevation = 1.dp, 38 | colors = colors, 39 | ) 40 | } 41 | 42 | @Composable 43 | fun SettingsItem( 44 | onClick: () -> Unit, 45 | modifier: Modifier = Modifier, 46 | title: @Composable () -> Unit, 47 | description: (@Composable () -> Unit)? = null, 48 | trailing: (@Composable () -> Unit)? = null, 49 | icon: (@Composable () -> Unit)? = null, 50 | enabled: Boolean = true, 51 | ) { 52 | SettingsItem( 53 | modifier = modifier 54 | .clickable( 55 | onClick = onClick, 56 | enabled = enabled 57 | ), 58 | icon = icon, 59 | description = description, 60 | title = title, 61 | trailing = trailing, 62 | enabled = enabled, 63 | ) 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/settings/component/SettingsNavigateItem.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.settings.component 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.material3.Icon 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.res.painterResource 8 | import com.xinto.mauth.R 9 | 10 | @Composable 11 | fun SettingsNavigateItem( 12 | modifier: Modifier = Modifier, 13 | onClick: () -> Unit, 14 | title: @Composable () -> Unit, 15 | description: (@Composable () -> Unit)? = null, 16 | icon: (@Composable () -> Unit)? = null, 17 | enabled: Boolean = true, 18 | ) { 19 | SettingsItem( 20 | modifier = modifier 21 | .clickable( 22 | enabled = enabled, 23 | onClick = onClick 24 | ), 25 | icon = icon, 26 | description = description, 27 | title = title, 28 | trailing = { 29 | Icon( 30 | painter = painterResource(R.drawable.ic_navigate_next), 31 | contentDescription = null 32 | ) 33 | }, 34 | enabled = enabled 35 | ) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/settings/component/SettingsSwitchItem.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.settings.component 2 | 3 | import androidx.compose.foundation.selection.toggleable 4 | import androidx.compose.material3.Switch 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | 8 | @Composable 9 | fun SettingsSwitchItem( 10 | modifier: Modifier = Modifier, 11 | onCheckedChange: ((Boolean) -> Unit)?, 12 | checked: Boolean, 13 | title: @Composable () -> Unit, 14 | description: (@Composable () -> Unit)? = null, 15 | icon: (@Composable () -> Unit)? = null, 16 | thumbContent: (@Composable () -> Unit)? = null, 17 | enabled: Boolean = true, 18 | ) { 19 | val toggleableModifier = if (onCheckedChange != null) { 20 | Modifier.toggleable( 21 | value = checked, 22 | enabled = enabled, 23 | onValueChange = onCheckedChange 24 | ) 25 | } else Modifier 26 | 27 | SettingsItem( 28 | modifier = modifier 29 | .then(toggleableModifier), 30 | icon = icon, 31 | description = description, 32 | title = title, 33 | trailing = { 34 | Switch( 35 | checked = checked, 36 | onCheckedChange = onCheckedChange, 37 | enabled = enabled, 38 | thumbContent = thumbContent 39 | ) 40 | }, 41 | enabled = enabled 42 | ) 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/screen/theme/ThemeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.screen.theme 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.xinto.mauth.core.settings.model.ColorSetting 6 | import com.xinto.mauth.core.settings.model.ThemeSetting 7 | import com.xinto.mauth.domain.SettingsRepository 8 | import kotlinx.coroutines.flow.SharingStarted 9 | import kotlinx.coroutines.flow.stateIn 10 | import kotlinx.coroutines.launch 11 | 12 | class ThemeViewModel( 13 | private val settingsRepository: SettingsRepository 14 | ) : ViewModel() { 15 | 16 | val theme = settingsRepository.getTheme() 17 | .stateIn( 18 | scope = viewModelScope, 19 | initialValue = ThemeSetting.DEFAULT, 20 | started = SharingStarted.WhileSubscribed(5000) 21 | ) 22 | 23 | val color = settingsRepository.getColor() 24 | .stateIn( 25 | scope = viewModelScope, 26 | initialValue = ColorSetting.DEFAULT, 27 | started = SharingStarted.WhileSubscribed(5000) 28 | ) 29 | 30 | fun updateTheme(newTheme: ThemeSetting) { 31 | viewModelScope.launch { 32 | settingsRepository.setTheme(newTheme) 33 | } 34 | } 35 | 36 | fun updateColor(newColor: ColorSetting) { 37 | viewModelScope.launch { 38 | settingsRepository.setColor(newColor) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.theme 2 | 3 | import android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.dynamicDarkColorScheme 7 | import androidx.compose.material3.dynamicLightColorScheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.platform.LocalInspectionMode 11 | import com.xinto.mauth.core.settings.model.ColorSetting 12 | import com.xinto.mauth.core.settings.model.ThemeSetting 13 | import com.xinto.mauth.ui.theme.color.BlueberryBlueDark 14 | import com.xinto.mauth.ui.theme.color.LimeGreenDark 15 | import com.xinto.mauth.ui.theme.color.MothPurpleDark 16 | import com.xinto.mauth.ui.theme.color.OrangeOrangeDark 17 | import com.xinto.mauth.ui.theme.color.SkyCyanDark 18 | import com.xinto.mauth.ui.theme.color.LemonYellowDark 19 | import com.xinto.mauth.ui.theme.color.BlueberryBlueLight 20 | import com.xinto.mauth.ui.theme.color.LemonYellowLight 21 | import com.xinto.mauth.ui.theme.color.LimeGreenLight 22 | import com.xinto.mauth.ui.theme.color.MothPurpleLight 23 | import com.xinto.mauth.ui.theme.color.OrangeOrangeLight 24 | import com.xinto.mauth.ui.theme.color.SkyCyanLight 25 | 26 | @Composable 27 | fun MauthTheme( 28 | theme: ThemeSetting = ThemeSetting.DEFAULT, 29 | color: ColorSetting = ColorSetting.DEFAULT, 30 | content: @Composable () -> Unit 31 | ) { 32 | val isDark = when (theme) { 33 | ThemeSetting.System -> isSystemInDarkTheme() 34 | ThemeSetting.Dark -> true 35 | ThemeSetting.Light -> false 36 | } 37 | val isInPreview = LocalInspectionMode.current 38 | val colorScheme = when { 39 | color == ColorSetting.Dynamic && (isInPreview || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> { 40 | val context = LocalContext.current 41 | when (isDark) { 42 | true -> dynamicDarkColorScheme(context) 43 | false -> dynamicLightColorScheme(context) 44 | } 45 | } 46 | color == ColorSetting.BlueberryBlue -> when (isDark) { 47 | true -> BlueberryBlueDark 48 | false -> BlueberryBlueLight 49 | } 50 | color == ColorSetting.PickleYellow -> when (isDark) { 51 | true -> LemonYellowDark 52 | false -> LemonYellowLight 53 | } 54 | color == ColorSetting.ToxicGreen -> when (isDark) { 55 | true -> LimeGreenDark 56 | false -> LimeGreenLight 57 | } 58 | color == ColorSetting.LeatherOrange -> when (isDark) { 59 | true -> OrangeOrangeDark 60 | false -> OrangeOrangeLight 61 | } 62 | color == ColorSetting.OceanTurquoise -> when (isDark) { 63 | true -> SkyCyanDark 64 | false -> SkyCyanLight 65 | } 66 | else -> when (isDark) { 67 | true -> MothPurpleDark 68 | false -> MothPurpleLight 69 | } 70 | } 71 | MaterialTheme( 72 | colorScheme = colorScheme, 73 | typography = Typography, 74 | content = content 75 | ) 76 | } -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val Typography = Typography() -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/mauth/util/Coroutines.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.mauth.util 2 | 3 | import androidx.lifecycle.Lifecycle 4 | import androidx.lifecycle.coroutineScope 5 | import androidx.lifecycle.flowWithLifecycle 6 | import kotlinx.coroutines.Job 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.catch 9 | import kotlinx.coroutines.flow.launchIn 10 | import kotlinx.coroutines.flow.onEach 11 | 12 | inline fun Flow.catchMap( 13 | crossinline transformation: (Throwable) -> T 14 | ): Flow { 15 | return catch { 16 | emit(transformation(it)) 17 | } 18 | } 19 | 20 | fun Flow.launchInLifecycle( 21 | lifecycle: Lifecycle, 22 | state: Lifecycle.State = Lifecycle.State.STARTED, 23 | action: suspend (T) -> Unit 24 | ): Job { 25 | return flowWithLifecycle(lifecycle, state) 26 | .onEach(action) 27 | .launchIn(lifecycle.coroutineScope) 28 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_a_photo.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_apartment.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_downward.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_upward.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_backspace.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_brush.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bug.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_contrast.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_copy_all.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_forever.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_empty_dashboard.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_export.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_fingerprint.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_github.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_key.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_label.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_moon.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_vert.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_navigate_next.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_password.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_qr_code_2.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_qr_code_scanner.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sort.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sun.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_undo.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visibility.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_visibility_off.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-tr-rTR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Burada henüz bir şey yok 3 | Bir hesap ekle 4 | QR kodunu tarayın veya anahtarı manuel olarak girin 5 | QR kodunu tarayın 6 | Bir resim seçin 7 | Verileri manuel olarak girin 8 | Hesaplar silinsin mi? 9 | Seçilen hesaplar kalıcı olarak silinecek. 10 | Sil 11 | İptal 12 | 13 | QR kodunu tarayın 14 | Eksik izinler 15 | Kamera izni verilmedi. 16 | Bu uygulamanın QR kodunu taraması ve analiz etmesi için kamera izni gerekir, Kamera izni olmadan Mauth hesaplarınızı tarayamaz ve içe aktaramaz. 17 | İzin Ver 18 | İptal Et 19 | 20 | Bir hesap ekle 21 | Bir hesabı düzenleyin 22 | Kaydet 23 | Etiket 24 | Issuer 25 | Secret 26 | Type 27 | Algoritma 28 | Rakamlar 29 | Counter 30 | Period 31 | Değişiklikleri iptal Et 32 | Değişiklikleriniz kaydedilmeyecek. 33 | Discard 34 | İptal Et 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #420042 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") version "8.7.2" apply false 3 | kotlin("android") version "2.0.21" apply false 4 | kotlin("plugin.compose") version "2.0.21" apply false 5 | id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false 6 | } -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | v0.1.0: 2 | - Initial release! 3 | - TOTP Support 4 | - Manual account add support 5 | - Dynamic theming via Material You -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/20.txt: -------------------------------------------------------------------------------- 1 | v0.2.0: 2 | - Revamped the home screen. 3 | - Implemented individual timers for each account. 4 | - Issuers are now visible on the account cards. 5 | - Added icons to the account cards. 6 | - Finished the "Add Account" screen. 7 | - Added the QR code scanner. 8 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/21.txt: -------------------------------------------------------------------------------- 1 | v0.2.1: 2 | A hotfix release. 3 | - Prevent the camera from closing (freezing) when an invalid QR code is detected. 4 | - Fixed a crash that occurs when the URI label is not provided. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/30.txt: -------------------------------------------------------------------------------- 1 | v0.3.0: 2 | - Added an ability to select and delete accounts. 3 | - Added Norwegian Bokmål language. 4 | - Removed the placeholders from the text fields in the account add screen. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/40.txt: -------------------------------------------------------------------------------- 1 | v0.4.0: 2 | - Add account editing. 3 | - Add HOTP support. 4 | - Fix dropdown menus in the account add/edit menu. 5 | - Fix the account icon not resetting after re-entering the account add/edit menu. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/50.txt: -------------------------------------------------------------------------------- 1 | v0.5.0 2 | 3 | New features 4 | ============ 5 | - Added a settings page 6 | - Includes a "Secure Mode" switch that protects you from exposing data via screenshots 7 | - `otpauth://` deeplink support 8 | - Added a splash screen for devices running on Android 11 and below 9 | - Turkish translation 10 | 11 | Changes 12 | ======= 13 | - Small UI updates 14 | - "Full-screen" immersive navigation and top bars 15 | - Animated code display 16 | - Remove placeholder icon buttons in home bottom bar 17 | - Updates to account card 18 | - 90% of the app was rewritten from scratch! This should result in a better performance and will make subsequent updates much easier 19 | 20 | Bug fixes 21 | ========= 22 | - Fixed a bug where camera would not properly close after exiting the QR Scan screen 23 | - Fixed many possible crashes 24 | 25 | *Note: due to changes to the app's database, a complex migration was written to convert from the old schema to the new one. Please report issues if Mauth starts crashing or accounts disappear after updating to v0.5.0* -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/51.txt: -------------------------------------------------------------------------------- 1 | v0.5.1 2 | 3 | Changes 4 | ======= 5 | - Updated the account screen 6 | - The number fields will now turn red when an incorrect number is entered 7 | - Required fields are now marked 8 | - The save button will be greyed out until you satisfy all the requirements 9 | 10 | Fixes 11 | ===== 12 | - Fixed a crash when editing number fields in the account screen 13 | - Fixed a crash when saving the edited account in the account screen -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/52.txt: -------------------------------------------------------------------------------- 1 | v0.5.2 2 | 3 | Changes 4 | ======= 5 | - Implement form validation for number fields in the account screen 6 | - Fields are now coerced to a specific range 7 | - Digits field's ranges are annotated 8 | 9 | Fixes 10 | ===== 11 | - Fixed a bug where Mauth would accept any value for number fields in the Account screen, rendering the app unusable -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/53.txt: -------------------------------------------------------------------------------- 1 | v0.5.3 2 | 3 | Fixes 4 | ===== 5 | - Fixed the form validation in the account screen. Again! -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/60.txt: -------------------------------------------------------------------------------- 1 | v0.6.0 2 | 3 | New Features 4 | ============ 5 | - You can now sort your accounts by label, issuer and addition date (both ascending and descending) 6 | - The app's icon now supports Material You colors 7 | - The accounts now store their addition/creation date 8 | 9 | Fixes 10 | ===== 11 | - Fixed a bug where Android's clipboard preview would show the account code, even if it was hidden 12 | - Updated the Add Account bottom sheet for consistency with the Material 3 guidelines 13 | - Minor bottom bar menu fixes 14 | - Refactored and optimized some parts of the code 15 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/61.txt: -------------------------------------------------------------------------------- 1 | v0.6.1 2 | 3 | Fixes 4 | ===== 5 | - Fixed the parsing for OTP strings that had lowercase algorithm names 6 | - The buttons in the "Add Account" bottom sheet no longer overlap with the system navigation bar -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/70.txt: -------------------------------------------------------------------------------- 1 | New Features 2 | ============ 3 | - Added Chinese (Simplified) translation 4 | - Added Portuguese translation 5 | - Implemented authentication methods 6 | - PIN Authentication 7 | - Biometric Authentication (requires PIN auth to be enabled) 8 | - Added the About screen 9 | - The About screen includes a shortcut that navigates you to the repository's feedback (issues) page 10 | 11 | Changes 12 | ======= 13 | - Updated navigation transitions to be more fluid 14 | - Other small UI tweaks 15 | 16 | Note: Please consider reporting any bugs regarding the new pin and biometric authentication on Mauth's feedback page. The more security issues I fix before v1.0.0, the safer it'll be for people to use Mauth. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/80.txt: -------------------------------------------------------------------------------- 1 | New Features 2 | ============ 3 | - Added theme settings, allowing you to customize how Mauth looks like 4 | 5 | Changes 6 | ========= 7 | - Updated pt-BR translation 8 | - Updated Chinese (Simplified) translation 9 | - The "Add account" buttons now have the same color 10 | - Rephrased the sorting option labels 11 | 12 | Fixes 13 | ===== 14 | - Fixed auth pin button colors and behavior 15 | - Fixed a bug that allowed empty PINs to be set 16 | - The edit button should no longer disappear when the label is too long 17 | - More fixes to the buttons in the "Add account" sheet 18 | - Improved the OTP URI parser -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/90.txt: -------------------------------------------------------------------------------- 1 | New Features 2 | ============ 3 | - Added the export screen. It's possible to either export all accounts from the 3-dot menu, or export the selected accounts only from the selection menu. Exporting requires authentication if you have PIN lock enabled. 4 | - Added initial support for tablets and landscape mode. 5 | - Added Russian translation 6 | 7 | Changes 8 | ======= 9 | - Updated Norwegian Bokmål translation 10 | - Updated Chinese (Simplified) translation 11 | - Updated theme colors 12 | - Custom icons are copied to app's internal directory 13 | 14 | Fixes 15 | ===== 16 | - Fixed a blank screen appearing after scanning a QR code 17 | - Fixed custom icons not saving properly 18 | - Fixed performance issues with the home and account add screens 19 | - Other minor UI fixes -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Mauth (pronounced Moth) is a Two-Factor Authentication app with support for TOTP and HOTP and compatibility with Google Authenticator. It comes with a rich UI while also providing many necessary features. 2 | 3 | Features: 4 | - Compatible with Google Authenticator 5 | - Secure 6 | - Many ways to add your accounts 7 | - Algorithms 8 | - Organization 9 | - Export/Import (coming soon) 10 | 11 | Mauth uses latest Android technology (Jetpack Compose, CameraX, Biometrics, Room DB and more) to provide the best user experience with the least amount of bugs possible. It is in an active development but is nowhere near ready for daily usage. Use it at your own risk. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A Material You Two-factor Authentication app. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Mauth -------------------------------------------------------------------------------- /fastlane/metadata/android/ru/full_description.txt: -------------------------------------------------------------------------------- 1 | Mauth (произносится как моз) это двухфакторное приложение для аутентификации с поддержкой TOTP и HOTP и совместимое с Google Authenticator. Оно имеет красивый интерфейс, а также предоставляет много необходимых функций. 2 | 3 | Возможности: 4 | - Совместимо с Google Authenticator 5 | - Безопасное 6 | - Множество способов добавить свои учетные записи 7 | - Алгоритмы 8 | - Организация 9 | - Экспорт/импорт 10 | 11 | Mauth использует новейшие технологии Android (JetPack Compose, Camerax, Biometrics, Room DB и другие) чтобы обеспечить наилучший пользовательский опыт с наименьшим количеством ошибок. Оно находится в активной разработке, но ещё не готово к ежедневному использованию. Используйте его на свой страх и риск. -------------------------------------------------------------------------------- /fastlane/metadata/android/ru/short_description.txt: -------------------------------------------------------------------------------- 1 | Приложение для двухфакторной аутентификации в стиле Material You -------------------------------------------------------------------------------- /github/get_it_on_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/github/get_it_on_github.png -------------------------------------------------------------------------------- /github/mauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/github/mauth.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/X1nto/Mauth/3666bf540940b662bf5a922e6ba481e32e839e88/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 31 13:57:10 GET 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven(url = "https://jitpack.io") 14 | } 15 | } 16 | rootProject.name = "Mauth" 17 | include(":app") 18 | --------------------------------------------------------------------------------