diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..564ebe0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +*.g.dart +.env + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..62eb0c0 --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: android + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: ios + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..71d3dfc --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# EasyAuth + +Simple two factor authenticator + +Before building run + +flutter pub get +flutter pub run build_runner build \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..a997537 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,87 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 32 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "dev.fingertips.authenticator" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion 21 + targetSdkVersion 32 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { + debug { + applicationIdSuffix '.dev' + } + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..088a910 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/debug/res/values/strings.xml b/android/app/src/debug/res/values/strings.xml new file mode 100644 index 0000000..a95b3af --- /dev/null +++ b/android/app/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ + + + EasyAuth Dev + \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..30e29c2 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/dev/fingertips/authenticator/MainActivity.kt b/android/app/src/main/kotlin/dev/fingertips/authenticator/MainActivity.kt new file mode 100644 index 0000000..677b7fb --- /dev/null +++ b/android/app/src/main/kotlin/dev/fingertips/authenticator/MainActivity.kt @@ -0,0 +1,6 @@ +package dev.fingertips.authenticator + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png new file mode 100644 index 0000000..d276329 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2e82895 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png new file mode 100644 index 0000000..a784048 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..21b717d Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png new file mode 100644 index 0000000..005ef9c Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5d79466 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..64a11f2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4c26490 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..bb3f3e0 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ea4094e Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml new file mode 100644 index 0000000..7e91a57 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..9155055 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..58e9b8b Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..cb21317 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..9cd46d7 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..87dfb7a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..70a7f55 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + EasyAuth + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..088a910 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..83ae220 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.6.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cc5527d --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..574e76e Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/icon_background.png b/assets/icon_background.png new file mode 100644 index 0000000..0b93831 Binary files /dev/null and b/assets/icon_background.png differ diff --git a/assets/icon_foreground.png b/assets/icon_foreground.png new file mode 100644 index 0000000..82f9f5d Binary files /dev/null and b/assets/icon_foreground.png differ diff --git a/icon.xcf b/icon.xcf new file mode 100644 index 0000000..5ab77d1 Binary files /dev/null and b/icon.xcf differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..17946ee --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,481 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.fingertips.authenticator; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.fingertips.authenticator; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.fingertips.authenticator; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..5c95c07 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..9248afc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..4d177f7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..0e41793 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..bc7f311 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..4608a9a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..9ec4055 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..4d177f7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..d064d31 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..4afb16b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..4afb16b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..fbfb8c9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..3c361ab Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..a5efa75 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..670f9a8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..c8cc462 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Authenticator + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + EasyAuth + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + CFBundleLocalizations + + en + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/lib/app_themes.dart b/lib/app_themes.dart new file mode 100644 index 0000000..03f2ef9 --- /dev/null +++ b/lib/app_themes.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +enum AppTheme { + light, + dark +} + +final appThemeData = { + AppTheme.light: ThemeData( + brightness: Brightness.light, + primaryColor: Colors.blue, + extensions: const >[ + CustomColors.light + ], + ), + + AppTheme.dark: ThemeData( + brightness: Brightness.dark, + primaryColor: Colors.blue, + extensions: const >[ + CustomColors.dark + ], + ) +}; + +@immutable +class CustomColors extends ThemeExtension { + final Color? onAppBar; + + const CustomColors({ + required this.onAppBar, + }); + + @override + CustomColors copyWith({ + Color? onAppBar, + }) { + return CustomColors( + onAppBar: onAppBar ?? this.onAppBar + ); + } + + @override + CustomColors lerp(ThemeExtension? other, double t) { + if (other is! CustomColors) { + return this; + } + return CustomColors( + onAppBar: Color.lerp(onAppBar, other.onAppBar, t), + ); + } + + static const light = CustomColors( + onAppBar: Colors.black, + ); + + static const dark = CustomColors( + onAppBar: Colors.white, + ); +} \ No newline at end of file diff --git a/lib/database.dart b/lib/database.dart new file mode 100644 index 0000000..cbcfcce --- /dev/null +++ b/lib/database.dart @@ -0,0 +1,18 @@ +import 'package:authenticator/objectbox.g.dart'; +import 'package:authenticator/shared/domain/account.dart'; + +class Database { + static final Database _instance = Database._internal(); + factory Database() => _instance; + Database._internal(); + + late Store _store; + + late Box _accountBox; + Box get accountBox => _accountBox; + + Future init() async { + _store = await openStore(); + _accountBox = _store.box(); + } +} \ No newline at end of file diff --git a/lib/feature/account_list/data/account_list_cubit.dart b/lib/feature/account_list/data/account_list_cubit.dart new file mode 100644 index 0000000..642e573 --- /dev/null +++ b/lib/feature/account_list/data/account_list_cubit.dart @@ -0,0 +1,40 @@ +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/data/settings_repository.dart'; +import 'package:authenticator/shared/domain/account.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; + +part 'account_list_state.dart'; + +class AccountListCubit extends Cubit { + List _accountList = []; + late Stream> _accountStream; + + OrderType _orderType = OrderType.alphabetical; + + AccountListCubit() : super(const AccountListInitial()) { + _updateStream(); + + // Restore previous OrderType + GetIt.I().orderType().then((orderType) { + _orderType = orderType; + _updateStream(); + }); + } + + void setOrderType(OrderType orderType) { + _orderType = orderType; + _updateStream(); + // Save the new OrderType selection so it can be used as the default next launch + GetIt.I().setOrderType(orderType); + } + + void _updateStream() { + _accountStream = GetIt.I().streamAllAccounts(_orderType); + _accountStream.listen((newList) { + _accountList = newList; + emit(AccountListUpdated(_accountList)); + }); + } +} diff --git a/lib/feature/account_list/data/account_list_state.dart b/lib/feature/account_list/data/account_list_state.dart new file mode 100644 index 0000000..7809735 --- /dev/null +++ b/lib/feature/account_list/data/account_list_state.dart @@ -0,0 +1,21 @@ +part of 'account_list_cubit.dart'; + +abstract class AccountListState extends Equatable { + const AccountListState(); +} + +class AccountListInitial extends AccountListState { + const AccountListInitial() : super(); + + @override + List get props => []; +} + +class AccountListUpdated extends AccountListState { + final List accounts; + + const AccountListUpdated(this.accounts) : super(); + + @override + List get props => [accounts]; +} \ No newline at end of file diff --git a/lib/feature/account_list/ui/account_list_page.dart b/lib/feature/account_list/ui/account_list_page.dart new file mode 100644 index 0000000..cf5bbb2 --- /dev/null +++ b/lib/feature/account_list/ui/account_list_page.dart @@ -0,0 +1,264 @@ +import 'package:authenticator/app_themes.dart'; +import 'package:authenticator/feature/account_list/data/account_list_cubit.dart'; +import 'package:authenticator/feature/add_account/ui/add_by_qr_dialog.dart'; +import 'package:authenticator/feature/add_account/ui/add_by_setup_key_dialog.dart'; +import 'package:authenticator/feature/delete_account/ui/delete_account_dialog.dart'; +import 'package:authenticator/feature/settings/ui/settings_dialog.dart'; +import 'package:authenticator/feature/view_account/ui/view_account_dialog.dart'; +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/domain/account.dart'; +import 'package:authenticator/util/bottom_sheet.dart'; +import 'package:authenticator/util/color_generator.dart'; +import 'package:authenticator/widget/scroll_to_hide_widget.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AccountListPage extends StatefulWidget { + const AccountListPage({Key? key}) : super(key: key); + + @override + State createState() => _AccountListPageState(); +} + +class _AccountListPageState extends State { + final _controller = ScrollController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final customColors = Theme.of(context).extension()!; + + return BlocProvider( + create: (_) => AccountListCubit(), + child: BlocBuilder( + builder: (context, accountState) { + return Scaffold( + appBar: AppBar( + title: Text('EasyAuth', style: TextStyle(color: customColors.onAppBar),), + elevation: 0, + backgroundColor: Colors.transparent, + actions: [ + PopupMenuButton( + icon: Icon(Icons.sort, color: customColors.onAppBar,), + onSelected: (value) { + if (value != null && value is OrderType) { + BlocProvider.of(context).setOrderType(value); + } + }, + itemBuilder: (_) { + return const [ + PopupMenuItem( + value: OrderType.alphabetical, + child: Text('Title'), + ), + PopupMenuItem( + value: OrderType.newest, + child: Text('Newest'), + ), + PopupMenuItem( + value: OrderType.lastUsed, + child: Text('Last Used'), + ), + ]; + }, + ), + PopupMenuButton( + icon: Icon(Icons.more_vert, color: customColors.onAppBar,), + onSelected: (value) { + if (value == 'settings') { + BottomSheetHelper.show(context, const SettingsDialog()); + } + }, + itemBuilder: (_) { + return [ + const PopupMenuItem( + value: 'settings', + child: Text('Settings'), + ) + ]; + }, + ) + ], + ), + body: BlocBuilder( + builder: (_, accountState) { + if (accountState is AccountListUpdated) { + List accounts = accountState.accounts; + + if (accounts.isEmpty) { + return const Center( + child: Text('Tap a button below to add your first 2FA account'), + ); + } else { + return Padding( + padding: const EdgeInsets.all(8), + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + childAspectRatio: 3 / 1.5, + crossAxisSpacing: 20, + mainAxisSpacing: 20 + ), + controller: _controller, + itemCount: accounts.length, + itemBuilder: (context, index) { + return AccountCard(accounts[index], + onTap: () { + BottomSheetHelper.show(context, ViewAccountDialog(accounts[index].id)); + }, + onLongPress: () { + BottomSheetHelper.show(context, DeleteAccountDialog(accounts[index])); + }, + ); + }, + ), + ); + } + } else { + return const Center( + child: Text('Tap a button below to add your first 2FA account'), + ); + } + }, + ), + bottomNavigationBar: ScrollToHideWidget( + controller: _controller, + height: 48, // ScrollToHideWidget:height and AddButtons:height should be the same + child: AddButtons( + height: 48, + addQR: () { + BottomSheetHelper.show(context, const AddByQRDialog()) + .then((newId) { + if (newId != null) { + BottomSheetHelper.show(context, ViewAccountDialog(newId)); + } + }); + }, + addText: () { + BottomSheetHelper.show(context, const AddBySetupKeyDialog()) + .then((newId) { + if (newId != null) { + BottomSheetHelper.show(context, ViewAccountDialog(newId)); + } + }); + }, + ) + ), + ); + }, + ), + ); + } +} + +class AccountCard extends StatelessWidget { + final Account account; + final VoidCallback onTap; + final VoidCallback onLongPress; + + const AccountCard(this.account, {required this.onTap, + required this.onLongPress, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = MediaQuery.of(context).platformBrightness == Brightness.dark; + final cardColors = ColorGenerator.getColor(account.title); + final Color color; + if (isDarkMode) { + color = cardColors.cardDarkBackground; + } else { + color = cardColors.cardBackground; + } + + return Card( + color: color, + elevation: 4, + child: InkWell( + onTap: onTap, + onLongPress: onLongPress, + child: Padding( + padding: const EdgeInsets.all(4), + child: Center( + child: AutoSizeText(account.title, + style: const TextStyle(fontSize: 28), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 3, + ), + ), + ) + ), + ); + } +} + +class CardIcon extends StatelessWidget { + final String title; + + const CardIcon(this.title, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return CircleAvatar( + backgroundColor: ColorGenerator.getColor(title).iconBackground, + child: Center( + child: Text(title.substring(0, 1), style: const TextStyle(color: Colors.white),), + ), + ); + } +} + + +class AddButtons extends StatelessWidget { + final VoidCallback addQR; + final VoidCallback addText; + final double height; + + const AddButtons({required this.addQR, required this.addText, + this.height = 64, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + elevation: 40, + child: Container( + color: Theme.of(context).bottomAppBarColor, + height: height, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: TextButton( + onPressed: addQR, + child: Text('Add with QR Code', style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium?.color + ),), + ), + ), + Expanded( + child: TextButton( + onPressed: addText, + child: Text('Add with Setup Key', style: TextStyle( + color: Theme.of(context).textTheme.bodyMedium?.color + ),), + ) + ), + ], + ), + ), + ); + } +} diff --git a/lib/feature/add_account/data/add_by_qr_cubit.dart b/lib/feature/add_account/data/add_by_qr_cubit.dart new file mode 100644 index 0000000..8edf625 --- /dev/null +++ b/lib/feature/add_account/data/add_by_qr_cubit.dart @@ -0,0 +1,57 @@ +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/domain/account.dart'; +import 'package:authenticator/util/otp_uri.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; +import 'package:permission_handler/permission_handler.dart'; + +part 'add_by_qr_state.dart'; + +class AddByQRCubit extends Cubit { + final log = Logger('AddByQRCubit'); + + AddByQRCubit() : super(AddByQRInitial()); + + void checkCameraPermission() async { + final status = await Permission.camera.status; + if (status.isDenied) { + emit(CameraPermissionNotGranted()); + } else { + emit(CameraPermissionGranted()); + } + } + + void requestCameraPermission() async { + final isGranted = await Permission.camera.request().isGranted; + if (isGranted) { + emit(CameraPermissionGranted()); + } else { + emit(CameraPermissionDenied()); + } + } + + void openPermissionSettings() async { + await openAppSettings(); + checkCameraPermission(); + } + + void parseQRCode(String code) async { + try { + final otpData = OtpUri(code); + final account = Account( + title: otpData.issuer ?? 'New OTP Account', + secret: otpData.secret, + added: DateTime.now(), + lastUsed: DateTime.now() + ); + + int id = await GetIt.I().addAccount(account); + emit(AccountAddSuccessful(id)); + } on OtpUriError catch (e) { + log.warning(e.message); + emit(const AccountAddFailed('Invalid OTP QR code')); + } + } +} diff --git a/lib/feature/add_account/data/add_by_qr_state.dart b/lib/feature/add_account/data/add_by_qr_state.dart new file mode 100644 index 0000000..fc161c2 --- /dev/null +++ b/lib/feature/add_account/data/add_by_qr_state.dart @@ -0,0 +1,46 @@ +part of 'add_by_qr_cubit.dart'; + +abstract class AddByQRState extends Equatable { + const AddByQRState(); +} + +class AddByQRInitial extends AddByQRState { + @override + List get props => []; +} + +class CameraPermissionNotGranted extends AddByQRState { + @override + List get props => []; +} + +class CameraPermissionGranted extends AddByQRState { + @override + List get props => []; +} + +/// Differs from CameraPermissionNotGranted because this state is emitted +/// after a permission request fails, rather than the permission not being +/// granted from the initial request +class CameraPermissionDenied extends AddByQRState { + @override + List get props => []; +} + +class AccountAddSuccessful extends AddByQRState { + final int id; + + const AccountAddSuccessful(this.id); + + @override + List get props => [id]; +} + +class AccountAddFailed extends AddByQRState { + final String message; + + const AccountAddFailed(this.message); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/feature/add_account/data/add_by_setup_key_cubit.dart b/lib/feature/add_account/data/add_by_setup_key_cubit.dart new file mode 100644 index 0000000..e89d968 --- /dev/null +++ b/lib/feature/add_account/data/add_by_setup_key_cubit.dart @@ -0,0 +1,23 @@ +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/domain/account.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; + +part 'add_by_setup_key_state.dart'; + +class AddBySetupKeyCubit extends Cubit { + AddBySetupKeyCubit() : super(AddBySetupKeyInitial()); + + void onSubmit(String title, String secret) async { + final account = Account( + title: title, + secret: secret, + added: DateTime.now(), + lastUsed: DateTime.now() + ); + + final id = await GetIt.I().addAccount(account); + emit(AccountAddSuccessful(id)); + } +} diff --git a/lib/feature/add_account/data/add_by_setup_key_state.dart b/lib/feature/add_account/data/add_by_setup_key_state.dart new file mode 100644 index 0000000..8185b8d --- /dev/null +++ b/lib/feature/add_account/data/add_by_setup_key_state.dart @@ -0,0 +1,19 @@ +part of 'add_by_setup_key_cubit.dart'; + +abstract class AddBySetupKeyState extends Equatable { + const AddBySetupKeyState(); +} + +class AddBySetupKeyInitial extends AddBySetupKeyState { + @override + List get props => []; +} + +class AccountAddSuccessful extends AddBySetupKeyState { + final int id; + + const AccountAddSuccessful(this.id); + + @override + List get props => [id]; +} diff --git a/lib/feature/add_account/ui/add_by_qr_dialog.dart b/lib/feature/add_account/ui/add_by_qr_dialog.dart new file mode 100644 index 0000000..608d718 --- /dev/null +++ b/lib/feature/add_account/ui/add_by_qr_dialog.dart @@ -0,0 +1,149 @@ +import 'package:authenticator/feature/add_account/data/add_by_qr_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class AddByQRDialog extends StatefulWidget { + const AddByQRDialog({Key? key}) : super(key: key); + + @override + State createState() => _AddByQRDialogState(); +} + +class _AddByQRDialogState extends State { + late AddByQRCubit cubit = AddByQRCubit(); + + @override + Widget build(BuildContext context) { + double size = MediaQuery.of(context).size.width; + if (size >= 300) size = 300; + + return BlocProvider( + create: (_) => cubit, + child: Padding( + padding: const EdgeInsets.all(32), + child: BlocBuilder( + builder: (_, state) { + if (state is AccountAddFailed) { + return Camera( + showFailedMessage: true, + onCodeScanned: (code) { + cubit.parseQRCode(code); + }, + ); + } else if (state is AccountAddSuccessful) { + Navigator.of(context).pop(state.id); + return Container(); + } else if (state is CameraPermissionGranted) { + // Camera + return Camera( + onCodeScanned: (code) { + cubit.parseQRCode(code); + }, + ); + } else if (state is CameraPermissionNotGranted) { + // Ask for permission + return RequestPermission( + onPressed: () => cubit.requestCameraPermission() + ); + } else if (state is CameraPermissionDenied) { + // Ask again + return OpenPermissionSettings( + onPressed: () => cubit.openPermissionSettings() + ); + } else { + // No state yet + cubit.checkCameraPermission(); + return const Text('Loading'); + } + }, + ) + ), + ); + } +} + +class Camera extends StatelessWidget { + final bool showFailedMessage; + final Function(String) onCodeScanned; + + const Camera({required this.onCodeScanned, this.showFailedMessage = false, + Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + double size = MediaQuery.of(context).size.width; + if (size > 300) size = 300; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Align QR code with camera', style: TextStyle(fontSize: 24),), + const SizedBox(height: 12,), + (showFailedMessage) + ? const Text('There was a problem parsing the QR code', + style: TextStyle(color: Colors.red),) + : Container(), + Center( + child: SizedBox( + height: size, + width: size, + child: MobileScanner( + allowDuplicates: false, + onDetect: (barcode, args) { + if (barcode.rawValue != null) { + onCodeScanned(barcode.rawValue!); + } + }, + ), + ), + ) + ], + ); + } +} + +class RequestPermission extends StatelessWidget { + final VoidCallback onPressed; + + const RequestPermission({required this.onPressed, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Before scanning a QR code, please press the button below to allow access to your devices camera', + textAlign: TextAlign.center,), + ElevatedButton( + onPressed: onPressed, + child: const Text('Allow Camera Permission'), + ) + ], + ); + } +} + +class OpenPermissionSettings extends StatelessWidget { + final VoidCallback onPressed; + + const OpenPermissionSettings({required this.onPressed, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Camera permission is currently denied. Please press the button below to open Settings and allow the camera permission', + textAlign: TextAlign.center,), + ElevatedButton( + onPressed: onPressed, + child: const Text('Open Permission Settings'), + ) + ], + ); + } +} diff --git a/lib/feature/add_account/ui/add_by_setup_key_dialog.dart b/lib/feature/add_account/ui/add_by_setup_key_dialog.dart new file mode 100644 index 0000000..5c45632 --- /dev/null +++ b/lib/feature/add_account/ui/add_by_setup_key_dialog.dart @@ -0,0 +1,85 @@ +import 'package:authenticator/feature/add_account/data/add_by_setup_key_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; + +class AddBySetupKeyDialog extends StatefulWidget { + const AddBySetupKeyDialog({Key? key}) : super(key: key); + + @override + State createState() => _AddBySetupKeyDialogState(); +} + +class _AddBySetupKeyDialogState extends State { + final _formKey = GlobalKey(); + final TextEditingController _titleController = TextEditingController(), + _secretController = TextEditingController(); + + final cubit = AddBySetupKeyCubit(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => cubit, + child: BlocBuilder( + builder: (_, state) { + if (state is AccountAddSuccessful) { + Navigator.of(context).pop(state.id); + return Container(); + } else { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Add Account', style: TextStyle(fontSize: 32),), + FormBuilder( + key: _formKey, + child: Column( + children: [ + FormBuilderTextField( + name: 'title', + autofocus: true, + controller: _titleController, + decoration: const InputDecoration( + labelText: 'Title' + ), + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(errorText: 'This field is required') + ]), + ), + FormBuilderTextField( + name: 'secret', + controller: _secretController, + decoration: const InputDecoration( + labelText: 'Setup Key' + ), + obscureText: true, + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(errorText: 'This field is required') + ]), + ), + TextButton( + onPressed: () { + _formKey.currentState!.save(); + if (_formKey.currentState!.validate()) { + cubit.onSubmit( + _titleController.text, + _secretController.text); + } + }, + child: const Text('Submit'), + ) + ], + ), + ) + ], + ), + ); + } + }, + ), + ); + } +} diff --git a/lib/feature/delete_account/data/delete_account_cubit.dart b/lib/feature/delete_account/data/delete_account_cubit.dart new file mode 100644 index 0000000..75af69e --- /dev/null +++ b/lib/feature/delete_account/data/delete_account_cubit.dart @@ -0,0 +1,16 @@ +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/domain/account.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; + +part 'delete_account_state.dart'; + +class DeleteAccountCubit extends Cubit { + DeleteAccountCubit() : super(DeleteAccountInitial()); + + void delete(Account account) { + GetIt.I().deleteAccount(account); + emit(DeleteAccountComplete()); + } +} diff --git a/lib/feature/delete_account/data/delete_account_state.dart b/lib/feature/delete_account/data/delete_account_state.dart new file mode 100644 index 0000000..ef511dd --- /dev/null +++ b/lib/feature/delete_account/data/delete_account_state.dart @@ -0,0 +1,15 @@ +part of 'delete_account_cubit.dart'; + +abstract class DeleteAccountState extends Equatable { + const DeleteAccountState(); +} + +class DeleteAccountInitial extends DeleteAccountState { + @override + List get props => []; +} + +class DeleteAccountComplete extends DeleteAccountState { + @override + List get props => []; +} diff --git a/lib/feature/delete_account/ui/delete_account_dialog.dart b/lib/feature/delete_account/ui/delete_account_dialog.dart new file mode 100644 index 0000000..1d7b50f --- /dev/null +++ b/lib/feature/delete_account/ui/delete_account_dialog.dart @@ -0,0 +1,47 @@ +import 'package:authenticator/feature/delete_account/data/delete_account_cubit.dart'; +import 'package:authenticator/shared/domain/account.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DeleteAccountDialog extends StatefulWidget { + final Account account; + + const DeleteAccountDialog(this.account, {Key? key}) : super(key: key); + + @override + State createState() => _DeleteAccountDialogState(); +} + +class _DeleteAccountDialogState extends State { + final cubit = DeleteAccountCubit(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => cubit, + child: Padding( + padding: const EdgeInsets.all(24), + child: BlocBuilder( + builder: (_, state) { + if (state is DeleteAccountComplete) { + Navigator.of(context).pop(); + return Container(); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Delete this account? This cannot be undone. Make sure to disable 2FA from the accounts settings before continuing.'), + MaterialButton( + onPressed: () => cubit.delete(widget.account), + color: Colors.red, + child: const Text('Delete this account'), + ) + ], + ); + } + } + ), + ) + ); + } +} diff --git a/lib/feature/settings/data/settings_cubit.dart b/lib/feature/settings/data/settings_cubit.dart new file mode 100644 index 0000000..2a07a62 --- /dev/null +++ b/lib/feature/settings/data/settings_cubit.dart @@ -0,0 +1,46 @@ +import 'package:authenticator/shared/data/settings_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; + +part 'settings_state.dart'; + +class SettingsCubit extends Cubit { + bool _requireBiometrics = false; + bool _automaticallyCopyOtp = false; + + late SettingsRepository _settings; + + SettingsCubit() : super(SettingsInitial()) { + _settings = GetIt.I(); + _loadInitialSettings(); + } + + Future _loadInitialSettings() async { + _requireBiometrics = await _settings.useBiometrics(); + _automaticallyCopyOtp = await _settings.autoCopyOtp(); + + emit(SettingsUpdate( + requireBiometrics: _requireBiometrics, + automaticallyCopyOtp: _automaticallyCopyOtp + )); + } + + void setRequireBiometrics(bool newValue) async { + await _settings.setUseBiometrics(newValue); + _requireBiometrics = newValue; + emit(SettingsUpdate( + requireBiometrics: _requireBiometrics, + automaticallyCopyOtp: _automaticallyCopyOtp + )); + } + + void setAutomaticallyCopyOtp(bool newValue) async { + await _settings.setAutoCopyOtp(newValue); + _automaticallyCopyOtp = newValue; + emit(SettingsUpdate( + requireBiometrics: _requireBiometrics, + automaticallyCopyOtp: _automaticallyCopyOtp + )); + } +} diff --git a/lib/feature/settings/data/settings_state.dart b/lib/feature/settings/data/settings_state.dart new file mode 100644 index 0000000..321b9d3 --- /dev/null +++ b/lib/feature/settings/data/settings_state.dart @@ -0,0 +1,21 @@ +part of 'settings_cubit.dart'; + +abstract class SettingsState extends Equatable { + const SettingsState(); +} + +class SettingsInitial extends SettingsState { + @override + List get props => []; +} + +class SettingsUpdate extends SettingsState { + final bool requireBiometrics; + final bool automaticallyCopyOtp; + + const SettingsUpdate({required this.requireBiometrics, + required this.automaticallyCopyOtp}); + + @override + List get props => [requireBiometrics, automaticallyCopyOtp]; +} diff --git a/lib/feature/settings/ui/settings_dialog.dart b/lib/feature/settings/ui/settings_dialog.dart new file mode 100644 index 0000000..a4a97a9 --- /dev/null +++ b/lib/feature/settings/ui/settings_dialog.dart @@ -0,0 +1,49 @@ +import 'package:authenticator/feature/settings/data/settings_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsDialog extends StatefulWidget { + const SettingsDialog({Key? key}) : super(key: key); + + @override + State createState() => _SettingsDialogState(); +} + +class _SettingsDialogState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => SettingsCubit() + ), + ], + child: BlocBuilder( + builder: (context, settingsState) { + if (settingsState is SettingsUpdate) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Settings', style: Theme.of(context).textTheme.headline3,), + const SizedBox(height: 12,), + SwitchListTile( + title: const Text('Automatically copy new OTP'), + secondary: const Icon(Icons.copy), + value: settingsState.automaticallyCopyOtp, + onChanged: (value) { + BlocProvider.of(context).setAutomaticallyCopyOtp(value); + }, + ), + ], + ); + } else { + return Container(); + } + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/feature/view_account/data/otp_cubit.dart b/lib/feature/view_account/data/otp_cubit.dart new file mode 100644 index 0000000..27a1306 --- /dev/null +++ b/lib/feature/view_account/data/otp_cubit.dart @@ -0,0 +1,34 @@ +import 'package:authenticator/feature/view_account/data/otp_generator.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'otp_state.dart'; + +class OtpCubit extends Cubit { + final String secret; + + late OtpGenerator generator; + + String? otp; + int? second; + + OtpCubit(this.secret) : super(OtpInitial()) { + generator = OtpGenerator(secret); + generator.otpCodeController.stream.listen((newOtp) { + otp = newOtp; + if (second != null) { + emit(OtpUpdate(newOtp, second!)); + } + }); + generator.secondsRemainingController.stream.listen((newSecond) { + second = newSecond; + if (otp != null) { + emit(OtpUpdate(otp!, newSecond)); + } + }); + } + + void dispose() { + generator.dispose(); + } +} diff --git a/lib/feature/view_account/data/otp_generator.dart b/lib/feature/view_account/data/otp_generator.dart new file mode 100644 index 0000000..9570226 --- /dev/null +++ b/lib/feature/view_account/data/otp_generator.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +import 'package:otp/otp.dart'; + +class OtpGenerator { + final otpCodeController = StreamController(); + final secondsRemainingController = StreamController(); + + String secret; + Timer? _secondsRemainingTimer; + bool _firstLoop = true; + + OtpGenerator(this.secret) { + _loopTimer(); + } + + void dispose() { + _secondsRemainingTimer?.cancel(); + otpCodeController.close(); + secondsRemainingController.close(); + } + + void _loopTimer() { + _secondsRemainingTimer?.cancel(); + otpCodeController.add(generateOtp()); + _updateSecondsRemaining(DateTime.now().second); + + _secondsRemainingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final now = DateTime.now(); + final currentSecond = now.second; + + _updateSecondsRemaining(currentSecond); + + if (currentSecond == 0 || currentSecond == 30 || _firstLoop) { + _firstLoop = false; + otpCodeController.add(generateOtp()); + } + }); + } + + void _updateSecondsRemaining(int currentSecond) { + final int secondsRemaining; + if (currentSecond > 30) { + secondsRemaining = 60 - currentSecond; + } else { + secondsRemaining = 30 - currentSecond; + } + + secondsRemainingController.add(secondsRemaining); + } + + String generateOtp() => OTP.generateTOTPCodeString( + secret, + DateTime.now().millisecondsSinceEpoch, + isGoogle: true, + algorithm: Algorithm.SHA1 + ); +} \ No newline at end of file diff --git a/lib/feature/view_account/data/otp_state.dart b/lib/feature/view_account/data/otp_state.dart new file mode 100644 index 0000000..b7f4d5f --- /dev/null +++ b/lib/feature/view_account/data/otp_state.dart @@ -0,0 +1,20 @@ +part of 'otp_cubit.dart'; + +abstract class OtpState extends Equatable { + const OtpState(); +} + +class OtpInitial extends OtpState { + @override + List get props => []; +} + +class OtpUpdate extends OtpState { + final String otp; + final int seconds; + + const OtpUpdate(this.otp, this.seconds); + + @override + List get props => [otp, seconds]; +} \ No newline at end of file diff --git a/lib/feature/view_account/data/view_account_cubit.dart b/lib/feature/view_account/data/view_account_cubit.dart new file mode 100644 index 0000000..f7bbef2 --- /dev/null +++ b/lib/feature/view_account/data/view_account_cubit.dart @@ -0,0 +1,28 @@ +import 'package:authenticator/feature/view_account/data/otp_generator.dart'; +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/domain/account.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; + +part 'view_account_state.dart'; + +class ViewAccountCubit extends Cubit { + ViewAccountCubit() : super(ViewAccountInitial()); + + late Account account; + + void loadAccount(int id) async { + final maybeAccount = await GetIt.I().getAccount(id); + if (maybeAccount == null) { + emit(AccountLoadError()); + } else { + account = maybeAccount; + emit(AccountLoaded(account)); + + // Update Account.lastUsed + account.lastUsed = DateTime.now(); + GetIt.I().updateAccount(account); + } + } +} diff --git a/lib/feature/view_account/data/view_account_state.dart b/lib/feature/view_account/data/view_account_state.dart new file mode 100644 index 0000000..5f528cc --- /dev/null +++ b/lib/feature/view_account/data/view_account_state.dart @@ -0,0 +1,24 @@ +part of 'view_account_cubit.dart'; + +abstract class ViewAccountState extends Equatable { + const ViewAccountState(); +} + +class ViewAccountInitial extends ViewAccountState { + @override + List get props => []; +} + +class AccountLoaded extends ViewAccountState { + final Account account; + + const AccountLoaded(this.account); + + @override + List get props => [account]; +} + +class AccountLoadError extends ViewAccountState { + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/feature/view_account/ui/view_account_dialog.dart b/lib/feature/view_account/ui/view_account_dialog.dart new file mode 100644 index 0000000..b073017 --- /dev/null +++ b/lib/feature/view_account/ui/view_account_dialog.dart @@ -0,0 +1,155 @@ +import 'dart:async'; + +import 'package:authenticator/feature/view_account/data/otp_cubit.dart'; +import 'package:authenticator/feature/view_account/data/view_account_cubit.dart'; +import 'package:authenticator/shared/data/settings_repository.dart'; +import 'package:authenticator/util/color_generator.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:percent_indicator/circular_percent_indicator.dart'; + +class ViewAccountDialog extends StatefulWidget { + final int id; + + const ViewAccountDialog(this.id, {Key? key}) : super(key: key); + + @override + State createState() => _ViewAccountDialogState(); +} + +class _ViewAccountDialogState extends State { + final accountCubit = ViewAccountCubit(); + OtpCubit? otpCubit; + + bool _showCopiedText = false; + + late final bool _autoCopyOtp; + String _lastCopiedOtp = ''; + + @override + void initState() { + super.initState(); + + // Get whether to auto copy new OTP codes + GetIt.I().autoCopyOtp().then((value) => _autoCopyOtp = value); + } + + @override + void dispose() { + otpCubit?.dispose(); + super.dispose(); + } + + void showCopiedText() { + setState(() => _showCopiedText = true); + Future.delayed(const Duration(seconds: 3), () { + setState(() => _showCopiedText = false); + }); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => accountCubit, + child: Padding( + padding: const EdgeInsets.all(24), + child: BlocBuilder( + builder: (_, state) { + if (state is AccountLoaded) { + final account = state.account; + final progressColor = ColorGenerator.getColor(account.title).timerFill; + + otpCubit = OtpCubit(account.secret); + return BlocProvider( + create: (_) => otpCubit!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText(state.account.title, + style: const TextStyle(fontSize: 52), + maxLines: 1 + ), + BlocBuilder( + builder: (_, state) { + if (state is OtpUpdate) { + // Auto copy new OTP if enabled + if (_autoCopyOtp && _lastCopiedOtp != state.otp) { + Clipboard.setData(ClipboardData(text: state.otp)); + _lastCopiedOtp = state.otp; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + OtpText(state.otp, + onTap: () { + Clipboard.setData(ClipboardData(text: state.otp)); + showCopiedText(); + } + ), + SecondsIndicator(state.seconds, progressColor), + ], + ); + } else { + return Container(); + } + } + ), + (_showCopiedText) + ? const Text('OTP code copied!') + : Container(), + ], + ), + ); + } else if (state is AccountLoadError) { + Navigator.pop(context); + return const Text('Account does not exist'); + } else { + accountCubit.loadAccount(widget.id); + return const Text('Loading'); + } + }, + ), + ), + ); + } +} + +class OtpText extends StatelessWidget { + final String otp; + final VoidCallback onTap; + + const OtpText(this.otp, {required this.onTap, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Text(otp, + style: const TextStyle(fontSize: 48), + ), + ); + } +} + +class SecondsIndicator extends StatelessWidget { + final int seconds; + final Color progressColor; + + const SecondsIndicator(this.seconds, this.progressColor, {Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CircularPercentIndicator( + radius: 30, + lineWidth: 8, + progressColor: progressColor, + percent: seconds / 30, + center: Text(seconds.toString(), style: const TextStyle(fontSize: 20),), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..ed72e1f --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,62 @@ +import 'package:authenticator/app_themes.dart'; +import 'package:authenticator/database.dart'; +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/data/account_repository_impl.dart'; +import 'package:authenticator/shared/data/settings_repository.dart'; +import 'package:authenticator/shared/data/settings_repository_impl.dart'; +import 'package:authenticator/util/bloc_observer.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:logging/logging.dart'; + +import 'feature/account_list/ui/account_list_page.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize logging + if (!kReleaseMode) { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + print('${record.loggerName}-${record.level.name}: ${record.time}: ${record.message}'); + }); + } + + // Initialize singletons + await Database().init(); + GetIt.I.registerSingleton + (AccountRepositoryImpl(Database().accountBox)); + + final preferences = SettingsRepositoryImpl(); + await preferences.init(); + GetIt.I.registerSingleton(preferences); + + // Start the app wrapped in a BlocObserver + BlocOverrides.runZoned(() => runApp(const MyApp()), + blocObserver: SimpleBlocObserver() + ); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Authenticator', + theme: appThemeData[AppTheme.light]!.copyWith( + extensions: >[ + CustomColors.light + ] + ), + darkTheme: appThemeData[AppTheme.dark]!.copyWith( + extensions: >[ + CustomColors.dark + ] + ), + home: const AccountListPage(), + ); + } +} \ No newline at end of file diff --git a/lib/objectbox-model.json b/lib/objectbox-model.json new file mode 100644 index 0000000..8bad55f --- /dev/null +++ b/lib/objectbox-model.json @@ -0,0 +1,52 @@ +{ + "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", + "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", + "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", + "entities": [ + { + "id": "1:3633394486228453716", + "lastPropertyId": "5:3575888530801387653", + "name": "Account", + "properties": [ + { + "id": "1:560531782921646148", + "name": "id", + "type": 6, + "flags": 1 + }, + { + "id": "2:6132860643781431573", + "name": "title", + "type": 9 + }, + { + "id": "3:1126271492582898995", + "name": "secret", + "type": 9 + }, + { + "id": "4:8678843706177953732", + "name": "added", + "type": 10 + }, + { + "id": "5:3575888530801387653", + "name": "lastUsed", + "type": 10 + } + ], + "relations": [] + } + ], + "lastEntityId": "1:3633394486228453716", + "lastIndexId": "0:0", + "lastRelationId": "0:0", + "lastSequenceId": "0:0", + "modelVersion": 5, + "modelVersionParserMinimum": 5, + "retiredEntityUids": [], + "retiredIndexUids": [], + "retiredPropertyUids": [], + "retiredRelationUids": [], + "version": 1 +} \ No newline at end of file diff --git a/lib/shared/data/account_repository.dart b/lib/shared/data/account_repository.dart new file mode 100644 index 0000000..4726648 --- /dev/null +++ b/lib/shared/data/account_repository.dart @@ -0,0 +1,16 @@ +import 'package:authenticator/shared/domain/account.dart'; + +enum OrderType { + alphabetical, + newest, + lastUsed +} + +abstract class AccountRepository { + Future> getAllAccounts(OrderType orderType); + Stream> streamAllAccounts(OrderType orderType); + Future getAccount(int id); + Future addAccount(Account account); + Future updateAccount(Account account); + Future deleteAccount(Account account); +} \ No newline at end of file diff --git a/lib/shared/data/account_repository_impl.dart b/lib/shared/data/account_repository_impl.dart new file mode 100644 index 0000000..29d6675 --- /dev/null +++ b/lib/shared/data/account_repository_impl.dart @@ -0,0 +1,68 @@ +import 'package:authenticator/objectbox.g.dart'; +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/domain/account.dart'; + +class AccountRepositoryImpl extends AccountRepository { + final Box _accountBox; + + AccountRepositoryImpl(this._accountBox); + + @override + Future getAccount(int id) async { + final query = _accountBox.query(Account_.id.equals(id)).build(); + final result = query.findFirst(); + query.close(); + return result; + } + + @override + Future> getAllAccounts(OrderType orderType) async { + + final query = _accountQueryBuilder(orderType).build(); + final results = query.find(); + + query.close(); + return results; + } + + @override + Stream> streamAllAccounts(OrderType orderType) async* { + final builder = _accountQueryBuilder(orderType); + final query = builder.watch(triggerImmediately: true); + await for (final q in query) { + yield q.find(); + } + } + + @override + Future addAccount(Account account) async => + _accountBox.put(account, mode: PutMode.insert); + + @override + Future updateAccount(Account account) async => + _accountBox.put(account, mode: PutMode.update); + + @override + Future deleteAccount(Account account) async => + _accountBox.remove(account.id); + + /// This builder will be needed for both the Future getAllAccounts + /// and the Stream streamAllAccounts + QueryBuilder _accountQueryBuilder(OrderType orderType) { + final builder = _accountBox.query(); + + switch (orderType) { + case OrderType.newest: + builder.order(Account_.added, flags: Order.descending); + break; + case OrderType.lastUsed: + builder.order(Account_.lastUsed, flags: Order.descending); + break; + default: + builder.order(Account_.title); + break; + } + + return builder; + } +} \ No newline at end of file diff --git a/lib/shared/data/settings_repository.dart b/lib/shared/data/settings_repository.dart new file mode 100644 index 0000000..4fdfed8 --- /dev/null +++ b/lib/shared/data/settings_repository.dart @@ -0,0 +1,16 @@ +import 'package:authenticator/shared/data/account_repository.dart'; + +abstract class SettingsRepository { + final String keyAutoCopyOtp = 'autoCopyOtp'; + final String keyUseBiometrics = 'useBiometrics'; + final String keyOrderType = 'orderType'; + + Future autoCopyOtp(); + Future setAutoCopyOtp(bool value); + + Future useBiometrics(); + Future setUseBiometrics(bool value); + + Future orderType(); + Future setOrderType(OrderType orderType); +} \ No newline at end of file diff --git a/lib/shared/data/settings_repository_impl.dart b/lib/shared/data/settings_repository_impl.dart new file mode 100644 index 0000000..3dda5d7 --- /dev/null +++ b/lib/shared/data/settings_repository_impl.dart @@ -0,0 +1,68 @@ +import 'package:authenticator/shared/data/account_repository.dart'; +import 'package:authenticator/shared/data/settings_repository.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsRepositoryImpl extends SettingsRepository { + late SharedPreferences _prefs; + + Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + @override + Future autoCopyOtp() async { + return _prefs.getBool(keyAutoCopyOtp) ?? false; + } + + @override + Future setAutoCopyOtp(bool value) async { + await _prefs.setBool(keyAutoCopyOtp, value); + } + + @override + Future useBiometrics() async { + return _prefs.getBool(keyUseBiometrics) ?? false; + } + + @override + Future setUseBiometrics(bool value) async { + await _prefs.setBool(keyUseBiometrics, value); + } + + static const String _orderTypeAlphabetical = 'alphabetical'; + static const String _orderTypeNewest = 'newest'; + static const String _orderTypeLastUsed = 'lastUsed'; + + @override + Future orderType() async { + final typeString = _prefs.getString(keyOrderType) ?? _orderTypeAlphabetical; + + switch (typeString) { + case _orderTypeNewest: + return OrderType.newest; + case _orderTypeLastUsed: + return OrderType.lastUsed; + default: + return OrderType.alphabetical; + } + } + + @override + Future setOrderType(OrderType orderType) async { + final String typeString; + + switch (orderType) { + case OrderType.newest: + typeString = _orderTypeNewest; + break; + case OrderType.lastUsed: + typeString = _orderTypeLastUsed; + break; + default: + typeString = _orderTypeAlphabetical; + break; + } + + await _prefs.setString(keyOrderType, typeString); + } +} \ No newline at end of file diff --git a/lib/shared/domain/account.dart b/lib/shared/domain/account.dart new file mode 100644 index 0000000..e644060 --- /dev/null +++ b/lib/shared/domain/account.dart @@ -0,0 +1,18 @@ +import 'package:objectbox/objectbox.dart'; + +@Entity() +class Account { + int id; + String title; + String secret; + DateTime added; + DateTime lastUsed; + + Account({ + this.id = 0, + required this.title, + required this.secret, + required this.added, + required this.lastUsed, + }); +} \ No newline at end of file diff --git a/lib/util/bloc_observer.dart b/lib/util/bloc_observer.dart new file mode 100644 index 0000000..9945fa9 --- /dev/null +++ b/lib/util/bloc_observer.dart @@ -0,0 +1,25 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logging/logging.dart'; + +/// A basic BLoC observer for debugging +class SimpleBlocObserver extends BlocObserver { + final log = Logger('SimpleBlocObserver'); + + @override + void onEvent(Bloc bloc, Object? event) { + super.onEvent(bloc, event); + log.info(event); + } + + @override + void onTransition(Bloc bloc, Transition transition) { + super.onTransition(bloc, transition); + log.info(transition); + } + + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + log.warning(error); + super.onError(bloc, error, stackTrace); + } +} \ No newline at end of file diff --git a/lib/util/bottom_sheet.dart b/lib/util/bottom_sheet.dart new file mode 100644 index 0000000..e9d289c --- /dev/null +++ b/lib/util/bottom_sheet.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class BottomSheetHelper { + + static Future show(BuildContext context, Widget child) async { + return await showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(25)) + ), + isScrollControlled: true, + builder: (context) { + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Wrap( + children: [child], + ), + ); + } + ); + } +} \ No newline at end of file diff --git a/lib/util/card_colors.dart b/lib/util/card_colors.dart new file mode 100644 index 0000000..5019964 --- /dev/null +++ b/lib/util/card_colors.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; + +enum ColorFor { + cardBackground, + cardDarkBackground, + iconBackground, + timerFill +} + +class CardColor extends ColorSwatch { + const CardColor(int primary, Map swatch) : super(primary, swatch); + + Color get cardBackground => this[ColorFor.cardBackground]!; + Color get cardDarkBackground => this[ColorFor.cardDarkBackground]!; + Color get iconBackground => this[ColorFor.iconBackground]!; + Color get timerFill => this[ColorFor.timerFill]!; +} + +class CardColors { + static const int _red = 0xFFffcdd2; + static const CardColor red = CardColor( + _red, + { + ColorFor.cardBackground: Color(_red), + ColorFor.cardDarkBackground: Color(0xFFb71c1c), + ColorFor.iconBackground: Color(0xFFef5350), + ColorFor.timerFill: Color(0xFFd32f2f) + } + ); + + static const int _pink = 0xFFf8bbd0; + static const CardColor pink = CardColor( + _pink, + { + ColorFor.cardBackground: Color(_pink), + ColorFor.cardDarkBackground: Color(0xFF880e4f), + ColorFor.iconBackground: Color(0xFFec407a), + ColorFor.timerFill: Color(0xFFc2185b) + } + ); + + static const int _purple = 0xFFe1bee7; + static const CardColor purple = CardColor( + _purple, + { + ColorFor.cardBackground: Color(_purple), + ColorFor.cardDarkBackground: Color(0xFF4a148c), + ColorFor.iconBackground: Color(0xFFab47bc), + ColorFor.timerFill: Color(0xFF7b1fa2) + } + ); + + static const int _deepPurple = 0xFFd1c4e9; + static const CardColor deepPurple = CardColor( + _deepPurple, + { + ColorFor.cardBackground: Color(_deepPurple), + ColorFor.cardDarkBackground: Color(0xFF311b92), + ColorFor.iconBackground: Color(0xFF7e57c2), + ColorFor.timerFill: Color(0xFF512da8) + } + ); + + static const int _indigo = 0xFFc5cae9; + static const CardColor indigo = CardColor( + _indigo, + { + ColorFor.cardBackground: Color(_indigo), + ColorFor.cardDarkBackground: Color(0xFF1a237e), + ColorFor.iconBackground: Color(0xFF5c6bc0), + ColorFor.timerFill: Color(0xFF303f9f) + } + ); + + static const int _blue = 0xFFbbdefb; + static const CardColor blue = CardColor( + _blue, + { + ColorFor.cardBackground: Color(_blue), + ColorFor.cardDarkBackground: Color(0xFF0d47a1), + ColorFor.iconBackground: Color(0xFF42a5f5), + ColorFor.timerFill: Color(0xFF1976d2) + } + ); + + static const int _lightBlue = 0xFFb3e5fc; + static const CardColor lightBlue = CardColor( + _lightBlue, + { + ColorFor.cardBackground: Color(_lightBlue), + ColorFor.cardDarkBackground: Color(0xFF01579b), + ColorFor.iconBackground: Color(0xFF29b6f6), + ColorFor.timerFill: Color(0xFF0288d1) + } + ); + + static const int _cyan = 0xFFb2ebf2; + static const CardColor cyan = CardColor( + _cyan, + { + ColorFor.cardBackground: Color(_cyan), + ColorFor.cardDarkBackground: Color(0xFF006064), + ColorFor.iconBackground: Color(0xFF26c6da), + ColorFor.timerFill: Color(0xFF0097a7) + } + ); + + static const int _teal = 0xFFb2dfdb; + static const CardColor teal = CardColor( + _teal, + { + ColorFor.cardBackground: Color(_teal), + ColorFor.cardDarkBackground: Color(0xFF004d40), + ColorFor.iconBackground: Color(0xFF26a69a), + ColorFor.timerFill: Color(0xFF00796b) + } + ); + + static const int _green = 0xFFc8e6c9; + static const CardColor green = CardColor( + _green, + { + ColorFor.cardBackground: Color(_green), + ColorFor.cardDarkBackground: Color(0xFF1b5e20), + ColorFor.iconBackground: Color(0xFF66bb6a), + ColorFor.timerFill: Color(0xFF388e3c) + } + ); + + static const int _lightGreen = 0xFFdcedc8; + static const CardColor lightGreen = CardColor( + _lightGreen, + { + ColorFor.cardBackground: Color(_lightGreen), + ColorFor.cardDarkBackground: Color(0xFF33691e), + ColorFor.iconBackground: Color(0xFF9ccc65), + ColorFor.timerFill: Color(0xFF689f38) + } + ); + + static const int _lime = 0xFFf0f4c3; + static const CardColor lime = CardColor( + _lime, + { + ColorFor.cardBackground: Color(_lime), + ColorFor.cardDarkBackground: Color(0xFF827717), + ColorFor.iconBackground: Color(0xFFd4e157), + ColorFor.timerFill: Color(0xFFafb42b) + } + ); + + static const int _yellow = 0xFFfff9c4; + static const CardColor yellow = CardColor( + _yellow, + { + ColorFor.cardBackground: Color(_yellow), + ColorFor.cardDarkBackground: Color(0xFFf57f17), + ColorFor.iconBackground: Color(0xFFffee58), + ColorFor.timerFill: Color(0xFFfbc02d) + } + ); + + static const int _amber = 0xFFffecb3; + static const CardColor amber = CardColor( + _amber, + { + ColorFor.cardBackground: Color(_amber), + ColorFor.cardDarkBackground: Color(0xFFff6f00), + ColorFor.iconBackground: Color(0xFFffca28), + ColorFor.timerFill: Color(0xFFffa000) + } + ); + + static const int _orange = 0xFFffe0b2; + static const CardColor orange = CardColor( + _orange, + { + ColorFor.cardBackground: Color(_orange), + ColorFor.cardDarkBackground: Color(0xFFe65100), + ColorFor.iconBackground: Color(0xFFffa726), + ColorFor.timerFill: Color(0xFFf57c00) + } + ); + + static const int _deepOrange = 0xFFffccbc; + static const CardColor deepOrange = CardColor( + _deepOrange, + { + ColorFor.cardBackground: Color(_deepOrange), + ColorFor.cardDarkBackground: Color(0xFFbf360c), + ColorFor.iconBackground: Color(0xFFff7043), + ColorFor.timerFill: Color(0xFFe64a19) + } + ); + + static const int _brown = 0xFFd7ccc8; + static const CardColor brown = CardColor( + _brown, + { + ColorFor.cardBackground: Color(_brown), + ColorFor.cardDarkBackground: Color(0xFF3e2723), + ColorFor.iconBackground: Color(0xFF8d6e63), + ColorFor.timerFill: Color(0xFF5d4037) + } + ); + + static const int _grey = 0xFFf5f5f5; + static const CardColor grey = CardColor( + _grey, + { + ColorFor.cardBackground: Color(_grey), + ColorFor.cardDarkBackground: Color(0xFF212121), + ColorFor.iconBackground: Color(0xFFbdbdbd), + ColorFor.timerFill: Color(0xFF616161) + } + ); + + static const int _blueGrey = 0xFFcfd8dc; + static const CardColor blueGrey = CardColor( + _blueGrey, + { + ColorFor.cardBackground: Color(_blueGrey), + ColorFor.cardDarkBackground: Color(0xFF263238), + ColorFor.iconBackground: Color(0xFF78909c), + ColorFor.timerFill: Color(0xFF455a64) + } + ); +} \ No newline at end of file diff --git a/lib/util/color_generator.dart b/lib/util/color_generator.dart new file mode 100644 index 0000000..a5f652d --- /dev/null +++ b/lib/util/color_generator.dart @@ -0,0 +1,29 @@ +import 'package:authenticator/util/card_colors.dart'; + +class ColorGenerator { + static const List _cardColors = [ + CardColors.red, + CardColors.pink, + CardColors.purple, + CardColors.deepPurple, + CardColors.indigo, + CardColors.blue, + CardColors.lightBlue, + CardColors.cyan, + CardColors.teal, + CardColors.green, + CardColors.lightGreen, + CardColors.lime, + CardColors.yellow, + CardColors.amber, + CardColors.orange, + CardColors.deepOrange, + CardColors.brown, + CardColors.grey, + CardColors.blueGrey + ]; + + static CardColor getColor(Object key) { + return _cardColors[(key.hashCode % _cardColors.length).abs()]; + } +} \ No newline at end of file diff --git a/lib/util/otp_uri.dart b/lib/util/otp_uri.dart new file mode 100644 index 0000000..46b997e --- /dev/null +++ b/lib/util/otp_uri.dart @@ -0,0 +1,52 @@ +class OtpUri { + String? _method; + String? get method => _method; + + late String _secret; + String get secret => _secret; + + String? _issuer; + String? get issuer => _issuer; + + String? _algorithm; + String? get algorithm => _algorithm; + + int? _digits; + int? get digits => _digits; + + int? _period; + int? get period => _period; + + + OtpUri(String otpUri) { + final uri = Uri.parse(otpUri); + + if (uri.scheme != 'otpauth') { + throw OtpUriError('Incorrect URI scheme'); + } + + if (!uri.queryParameters.containsKey('secret')) { + throw OtpUriError('Required parameter secret missing'); + } + + _method = uri.host; + _secret = uri.queryParameters['secret']!; + _issuer = uri.queryParameters['issuer']; + _algorithm = uri.queryParameters['algorithm']; + + if (uri.queryParameters.containsKey('digits')) { + _digits = int.tryParse(uri.queryParameters['digits']!); + } + + if (uri.queryParameters.containsKey('period')) { + _period = int.tryParse(uri.queryParameters['period']!); + } + + } +} + +class OtpUriError extends Error { + final String message; + + OtpUriError(this.message); +} \ No newline at end of file diff --git a/lib/widget/scroll_to_hide_widget.dart b/lib/widget/scroll_to_hide_widget.dart new file mode 100644 index 0000000..f55c4d0 --- /dev/null +++ b/lib/widget/scroll_to_hide_widget.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class ScrollToHideWidget extends StatefulWidget { + final Widget child; + final ScrollController controller; + final Duration duration; + final double height; + + const ScrollToHideWidget({ + Key? key, + required this.child, + required this.controller, + this.duration = const Duration(milliseconds: 200), + this.height = kBottomNavigationBarHeight, + }) : super(key: key); + + @override + State createState() => _ScrollToHideWidgetState(); +} + +class _ScrollToHideWidgetState extends State { + bool isVisible = true; + + @override + void initState() { + super.initState(); + + widget.controller.addListener(listen); + } + + @override + void dispose() { + widget.controller.removeListener(listen); + super.dispose(); + } + + void listen() { + final direction = widget.controller.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + show(); + } else if (direction == ScrollDirection.reverse) { + hide(); + } + } + + void show() { + if (!isVisible) setState(() => isVisible = true); + } + + void hide() { + if (isVisible) setState(() => isVisible = false); + } + + @override + Widget build(BuildContext context) => AnimatedContainer( + duration: widget.duration, + height: isVisible ? widget.height : 0, + child: Wrap(children: [widget.child],), + ); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..4fb5ea2 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,801 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "38.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "3.4.1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + base32: + dependency: "direct main" + description: + name: base32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + bloc: + dependency: transitive + description: + name: bloc + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.2" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + flat_buffers: + dependency: transitive + description: + name: flat_buffers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.1" + flutter_form_builder: + dependency: "direct main" + description: + name: flutter_form_builder + url: "https://pub.dartlang.org" + source: hosted + version: "7.3.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.3" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + form_builder_validators: + dependency: "direct main" + description: + name: form_builder_validators + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + get_it: + dependency: "direct main" + description: + name: get_it + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + logging: + dependency: "direct main" + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + objectbox: + dependency: "direct main" + description: + name: objectbox + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + objectbox_flutter_libs: + dependency: "direct main" + description: + name: objectbox_flutter_libs + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + objectbox_generator: + dependency: "direct dev" + description: + name: objectbox_generator + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + otp: + dependency: "direct main" + description: + name: otp + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.14" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.2" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "9.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + url: "https://pub.dartlang.org" + source: hosted + version: "9.0.2+1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + url: "https://pub.dartlang.org" + source: hosted + version: "9.0.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.7.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: transitive + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.15" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..2fe42a9 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,78 @@ +name: authenticator +description: Simple two factor authenticator + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 0.0.1+1 + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + auto_size_text: ^3.0.0 + base32: ^2.1.2 + equatable: ^2.0.3 + flutter: + sdk: flutter + flutter_bloc: ^8.0.1 + flutter_form_builder: ^7.3.0 + form_builder_validators: ^8.1.1 + get_it: ^7.2.0 + logging: ^1.0.2 + mobile_scanner: ^2.0.0 + objectbox: ^1.5.0 + objectbox_flutter_libs: ^1.5.0 + otp: ^3.0.4 + percent_indicator: ^4.2.2 + permission_handler: ^9.2.0 + shared_preferences: ^2.0.15 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + build_runner: ^2.1.11 + objectbox_generator: ^1.5.0 + flutter_launcher_icons: ^0.9.3 + +flutter_icons: + android: "launcher_icon" + ios: true + remove_alpha_ios: true + image_path: "assets/icon.png" + adaptive_icon_background: "assets/icon_background.png" + adaptive_icon_foreground: "assets/icon_foreground.png" + +flutter: + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..20a1be6 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:authenticator/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}